diff --git a/app.py b/app.py index b0a6b56..9d1ba4b 100644 --- a/app.py +++ b/app.py @@ -23,12 +23,24 @@ cache = Cache() def create_app(config_name=None): """Fábrica de aplicación Flask.""" - if config_name is None: - config_name = os.environ.get("FLASK_ENV", "default") - app = Flask(__name__) - app.config.from_object(config[config_name]) - config[config_name].init_app(app) + app.config["ENV"] = config_name # Añade esta línea si no existe + + # Debugging para ver qué está pasando + print(f"Configurando aplicación en modo: {config_name}") + print(f"ENV actual: {app.config['ENV']}") + + if isinstance(config_name, dict): + app.config.update(config_name) + else: + if config_name is None: + config_name = os.environ.get("FLASK_ENV", "default") + app.config.from_object(config[config_name]) + config[config_name].init_app(app) + + # Asegurar que el SECRET_KEY está configurado + if not app.config.get("SECRET_KEY"): + app.config["SECRET_KEY"] = "default_secret_key_please_change_in_production" # Inicializar extensiones login_manager.init_app(app) diff --git a/descripcion.md b/descripcion.md index bfd60d1..bd37810 100644 --- a/descripcion.md +++ b/descripcion.md @@ -103,7 +103,8 @@ La aplicación sigue una arquitectura basada en archivos JSON y sistema de fiche { "ESQ001": { "codigo": "ESQ001", - "descripcion": "Proyecto estándar", + "nombre": "Siemens", + "descripcion": "Proyecto estándar Siemens", "fecha_creacion": "2023-05-10T10:00:00Z", "creado_por": "admin", "documentos": [ @@ -553,130 +554,6 @@ Se implementará un plugin personalizado para pytest que generará reportes en f - Cobertura de código - Detalles de errores encontrados -### 9.5 Ejemplo de Configuración de Pytest - -```python -# conftest.py - -import pytest -import os -import json -import shutil -from app import create_app - -@pytest.fixture -def app(): - """Crear una instancia de la aplicación para pruebas.""" - # Configuración de prueba - test_config = { - 'TESTING': True, - 'STORAGE_PATH': 'test_storage', - 'SECRET_KEY': 'test_key' - } - - # Crear directorio de almacenamiento para pruebas - if not os.path.exists('test_storage'): - os.makedirs('test_storage') - - # Crear estructura básica - for dir in ['users', 'schemas', 'filetypes', 'projects', 'logs']: - if not os.path.exists(f'test_storage/{dir}'): - os.makedirs(f'test_storage/{dir}') - - # Crear app con configuración de prueba - app = create_app(test_config) - - yield app - - # Limpiar después de las pruebas - shutil.rmtree('test_storage') - -@pytest.fixture -def client(app): - """Cliente de prueba para la aplicación.""" - return app.test_client() - -@pytest.fixture -def auth(client): - """Helper para pruebas de autenticación.""" - class AuthActions: - def login(self, username='admin', password='password'): - return client.post('/login', data={ - 'username': username, - 'password': password - }, follow_redirects=True) - - def logout(self): - return client.get('/logout', follow_redirects=True) - - return AuthActions() -``` - -```python -# json_reporter.py - -import json -import pytest -import datetime -import os - -class JSONReporter: - def __init__(self, config): - self.config = config - self.results = { - 'summary': { - 'total': 0, - 'passed': 0, - 'failed': 0, - 'skipped': 0, - 'duration': 0, - 'timestamp': datetime.datetime.now().isoformat() - }, - 'tests': [] - } - - def pytest_runtest_logreport(self, report): - if report.when == 'call' or (report.when == 'setup' and report.skipped): - self.results['summary']['total'] += 1 - - if report.passed: - result = 'passed' - self.results['summary']['passed'] += 1 - elif report.failed: - result = 'failed' - self.results['summary']['failed'] += 1 - else: - result = 'skipped' - self.results['summary']['skipped'] += 1 - - self.results['tests'].append({ - 'name': report.nodeid, - 'result': result, - 'duration': report.duration, - 'error': str(report.longrepr) if hasattr(report, 'longrepr') and report.longrepr else None - }) - - def pytest_sessionfinish(self, session): - self.results['summary']['duration'] = session.config.hook.pytest_report_teststatus.get_duration() - - with open('test_results.json', 'w') as f: - json.dump(self.results, f, indent=2) - -@pytest.hookimpl(trylast=True) -def pytest_configure(config): - config.pluginmanager.register(JSONReporter(config), 'json_reporter') -``` - -## 10. Guía de Implementación - -### 10.1 Configuración del Entorno - -1. Crear entorno virtual: -```bash -python -m venv venv -source venv/bin/activate # Linux/Mac -venv\Scripts\activate # Windows -``` 2. Instalar dependencias: ```bash @@ -747,6 +624,7 @@ filetypes = { schemas = { "ESQ001": { "codigo": "ESQ001", + "nombre": "Siemens", "descripcion": "Proyecto estándar", "fecha_creacion": "2023-05-10T10:00:00Z", "creado_por": "admin", diff --git a/generate_test_report.py b/generate_test_report.py deleted file mode 100644 index 1674096..0000000 --- a/generate_test_report.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -""" -Simple script to generate a test report. -This is a simplified version of run_tests.py focused on generating the JSON report. -""" -import os -import subprocess -import sys -from datetime import datetime - - -def main(): - """Run tests and generate JSON report.""" - # Create directories if they don't exist - os.makedirs("test_reports", exist_ok=True) - - # Generate timestamp for current run - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - - print(f"Running tests and generating JSON report with timestamp: {timestamp}") - - # Run pytest with the JSON reporter plugin loaded - cmd = [ - "pytest", - "-v", - "--tb=short", - f"--junitxml=test_reports/junit_{timestamp}.xml", - "-p", - "tests.json_reporter", - "tests/", - ] - - result = subprocess.run(cmd, capture_output=False) - - if result.returncode == 0: - print("\nTests completed successfully!") - else: - print(f"\nTests completed with some failures (exit code: {result.returncode})") - - print( - f"JSON report should be available at: test_reports/test_results_{timestamp}.json" - ) - - return result.returncode - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/readme.md b/readme.md index f8108a2..96e8f48 100644 --- a/readme.md +++ b/readme.md @@ -112,6 +112,10 @@ Si encuentra algún problema o tiene alguna sugerencia, por favor abra un issue Este proyecto está licenciado bajo los términos de la licencia MIT. +## Running Tests + +To run the test suite: +``` Credenciales de acceso por defecto: Usuario: admin diff --git a/resultados_test.json b/resultados_test.json new file mode 100644 index 0000000..ee0ae02 --- /dev/null +++ b/resultados_test.json @@ -0,0 +1 @@ +{"created": 1741177873.7148027, "duration": 62.00483512878418, "exitcode": 1, "root": "D:\\Proyectos", "environment": {}, "summary": {"passed": 7, "failed": 5, "total": 12, "collected": 12}, "collectors": [{"nodeid": "", "outcome": "passed", "result": [{"nodeid": "Scripts/Arch/test/test_app.py", "type": "Module"}]}, {"nodeid": "Scripts/Arch/test/test_app.py::TestCase", "outcome": "passed", "result": []}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase", "outcome": "passed", "result": [{"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_document_count", "type": "TestCaseFunction", "lineno": 378}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_get_all_projects", "type": "TestCaseFunction", "lineno": 242}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_logging_setup", "type": "TestCaseFunction", "lineno": 93}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_archival", "type": "TestCaseFunction", "lineno": 462}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_creation", "type": "TestCaseFunction", "lineno": 98}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_deletion", "type": "TestCaseFunction", "lineno": 211}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_filtering", "type": "TestCaseFunction", "lineno": 288}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_hierarchy", "type": "TestCaseFunction", "lineno": 329}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_retrieval", "type": "TestCaseFunction", "lineno": 146}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_routes", "type": "TestCaseFunction", "lineno": 413}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_service_creation", "type": "TestCaseFunction", "lineno": 108}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_update", "type": "TestCaseFunction", "lineno": 171}]}, {"nodeid": "Scripts/Arch/test/test_app.py", "outcome": "passed", "result": [{"nodeid": "Scripts/Arch/test/test_app.py::TestCase", "type": "UnitTestCase"}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase", "type": "UnitTestCase"}]}], "tests": [{"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_document_count", "lineno": 378, "outcome": "passed", "keywords": ["test_document_count", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.0007327999919652939, "outcome": "passed"}, "call": {"duration": 5.098537599988049, "outcome": "passed", "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177812.2718973, "msecs": 271.0, "relativeCreated": 943.4635639190674, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:12,271"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177812.2718973, "msecs": 271.0, "relativeCreated": 943.4635639190674, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:12,271"}, {"name": "app", "msg": "Usuario administrador creado.", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\services\\auth_service.py", "filename": "auth_service.py", "module": "auth_service", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 200, "funcName": "initialize_admin_user", "created": 1741177812.2758958, "msecs": 275.0, "relativeCreated": 947.4620819091797, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:12,275"}, {"name": "app", "msg": "Esquemas predeterminados inicializados.", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\services\\schema_service.py", "filename": "schema_service.py", "module": "schema_service", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 158, "funcName": "initialize_default_schemas", "created": 1741177812.2768493, "msecs": 276.0, "relativeCreated": 948.4155178070068, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:12,276"}]}, "teardown": {"duration": 0.0003436999977566302, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_get_all_projects", "lineno": 242, "outcome": "failed", "keywords": ["test_get_all_projects", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.00042320002103224397, "outcome": "passed"}, "call": {"duration": 5.108540999994148, "outcome": "failed", "crash": {"path": "D:\\Proyectos\\Scripts\\Arch\\test\\test_app.py", "lineno": 287, "message": "AssertionError: assert 4 == 3\n + where 4 = len([{'ano_creacion': 2025, 'cliente': 'Document Client', 'codigo': 'PROJ001', 'creado_por': 'test_user', ...}, {'ano_crea... 'test_user', ...}, {'ano_creacion': 2025, 'cliente': 'Client 1', 'codigo': 'PROJ003', 'creado_por': 'test_user', ...}])"}, "traceback": [{"path": "test\\test_app.py", "lineno": 287, "message": "AssertionError"}], "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177817.351918, "msecs": 351.0, "relativeCreated": 6023.484230041504, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:17,351"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177817.3529341, "msecs": 352.0, "relativeCreated": 6024.500370025635, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:17,352"}], "longrepr": "self = \n\n def test_get_all_projects(self):\n \"\"\"Test retrieving all projects\"\"\"\n with self.app.app_context():\n # Limpiar proyectos existentes primero\n self.clean_existing_projects()\n \n # Create multiple projects\n projects_data = [\n {\n \"descripcion\": self.get_unique_name(\"Project 1\"),\n \"cliente\": \"Client 1\",\n \"esquema\": \"SCHEMA1\",\n },\n {\n \"descripcion\": self.get_unique_name(\"Project 2\"),\n \"cliente\": \"Client 2\",\n \"esquema\": \"SCHEMA1\",\n },\n {\n \"descripcion\": self.get_unique_name(\"Project 3\"),\n \"cliente\": \"Client 1\",\n \"esquema\": \"SCHEMA2\",\n },\n ]\n \n project_ids = []\n for data in projects_data:\n success, _, project_id = create_project(data, \"test_user\")\n assert success\n project_ids.append(project_id)\n \n # Mark one project as inactive\n delete_project(project_ids[1])\n \n # Get all active projects\n all_active_projects = get_all_projects(include_inactive=False)\n \n # Should only return 2 active projects\n assert len(all_active_projects) == 2\n \n # Get all projects including inactive\n all_projects = get_all_projects(include_inactive=True)\n \n # Should return all 3 projects\n> assert len(all_projects) == 3\nE AssertionError: assert 4 == 3\nE + where 4 = len([{'ano_creacion': 2025, 'cliente': 'Document Client', 'codigo': 'PROJ001', 'creado_por': 'test_user', ...}, {'ano_crea... 'test_user', ...}, {'ano_creacion': 2025, 'cliente': 'Client 1', 'codigo': 'PROJ003', 'creado_por': 'test_user', ...}])\n\ntest\\test_app.py:287: AssertionError"}, "teardown": {"duration": 0.0004185000143479556, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_logging_setup", "lineno": 93, "outcome": "passed", "keywords": ["test_logging_setup", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.000378099997760728, "outcome": "passed"}, "call": {"duration": 5.07875879999483, "outcome": "passed", "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177822.6156566, "msecs": 615.0, "relativeCreated": 11287.222862243652, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:22,615"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177822.6167204, "msecs": 616.0, "relativeCreated": 11288.286685943604, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:22,616"}]}, "teardown": {"duration": 0.0002895000216085464, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_archival", "lineno": 462, "outcome": "passed", "keywords": ["test_project_archival", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.0003798999823629856, "outcome": "passed"}, "call": {"duration": 5.087574700010009, "outcome": "passed", "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177827.6854231, "msecs": 685.0, "relativeCreated": 16356.98938369751, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:27,685"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177827.6854231, "msecs": 685.0, "relativeCreated": 16356.98938369751, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:27,685"}]}, "teardown": {"duration": 0.00045319998753257096, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_creation", "lineno": 98, "outcome": "passed", "keywords": ["test_project_creation", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.0003161999920848757, "outcome": "passed"}, "call": {"duration": 5.073090999998385, "outcome": "passed", "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177832.7766814, "msecs": 776.0, "relativeCreated": 21448.24767112732, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:32,776"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177832.777714, "msecs": 777.0, "relativeCreated": 21449.28026199341, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:32,777"}]}, "teardown": {"duration": 0.0003927999932784587, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_deletion", "lineno": 211, "outcome": "passed", "keywords": ["test_project_deletion", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.0003099000023212284, "outcome": "passed"}, "call": {"duration": 5.110509300022386, "outcome": "passed", "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177837.8573372, "msecs": 857.0, "relativeCreated": 26528.903484344482, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:37,857"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177837.8583465, "msecs": 858.0, "relativeCreated": 26529.91271018982, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:37,858"}]}, "teardown": {"duration": 0.0003865000035148114, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_filtering", "lineno": 288, "outcome": "failed", "keywords": ["test_project_filtering", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.0005765999958384782, "outcome": "passed"}, "call": {"duration": 5.134332400019048, "outcome": "failed", "crash": {"path": "D:\\Proyectos\\Scripts\\Arch\\test\\test_app.py", "lineno": 328, "message": "AssertionError: assert 'Web Developm...dbd130b6_2990' == 'Web Development'\n - Web Development\n + Web Development_dbd130b6_2990"}, "traceback": [{"path": "test\\test_app.py", "lineno": 328, "message": "AssertionError"}], "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177842.973509, "msecs": 973.0, "relativeCreated": 31645.07532119751, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:42,973"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177842.973509, "msecs": 973.0, "relativeCreated": 31645.07532119751, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:42,973"}], "longrepr": "self = \n\n def test_project_filtering(self):\n \"\"\"Test project filtering functionality\"\"\"\n with self.app.app_context():\n # Limpiar proyectos existentes primero\n self.clean_existing_projects()\n \n # Create projects with different characteristics\n projects_data = [\n {\n \"descripcion\": self.get_unique_name(\"Web Development\"),\n \"cliente\": \"Client A\",\n \"esquema\": \"SCHEMA1\",\n },\n {\n \"descripcion\": self.get_unique_name(\"Mobile App\"),\n \"cliente\": \"Client B\",\n \"esquema\": \"SCHEMA1\",\n },\n {\n \"descripcion\": self.get_unique_name(\"Desktop Application\"),\n \"cliente\": \"Client A\",\n \"esquema\": \"SCHEMA2\",\n },\n ]\n \n for data in projects_data:\n success, _, _ = create_project(data, \"test_user\")\n assert success\n \n # Test filtering by client\n client_filter = {\"cliente\": \"Client A\"}\n client_results = filter_projects(client_filter)\n assert len(client_results) == 2\n assert all(p[\"cliente\"] == \"Client A\" for p in client_results)\n \n # Test filtering by description\n desc_filter = {\"descripcion\": \"Web\"}\n desc_results = filter_projects(desc_filter)\n assert len(desc_results) == 1\n> assert desc_results[0][\"descripcion\"] == \"Web Development\"\nE AssertionError: assert 'Web Developm...dbd130b6_2990' == 'Web Development'\nE - Web Development\nE + Web Development_dbd130b6_2990\n\ntest\\test_app.py:328: AssertionError"}, "teardown": {"duration": 0.0004704000020865351, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_hierarchy", "lineno": 329, "outcome": "passed", "keywords": ["test_project_hierarchy", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.0004068000125698745, "outcome": "passed"}, "call": {"duration": 5.135443800012581, "outcome": "passed", "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177848.1209347, "msecs": 120.0, "relativeCreated": 36792.5009727478, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:48,120"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177848.121975, "msecs": 121.0, "relativeCreated": 36793.54119300842, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:48,121"}]}, "teardown": {"duration": 0.0003302999830339104, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_retrieval", "lineno": 146, "outcome": "failed", "keywords": ["test_project_retrieval", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.00032380002085119486, "outcome": "passed"}, "call": {"duration": 5.112968600005843, "outcome": "failed", "crash": {"path": "D:\\Proyectos\\Scripts\\Arch\\test\\test_app.py", "lineno": 168, "message": "AssertionError: assert 'Document Tes...90ea7319_2283' == 'Retrieval Test Project'\n - Retrieval Test Project\n + Document Test Project_90ea7319_2283"}, "traceback": [{"path": "test\\test_app.py", "lineno": 168, "message": "AssertionError"}], "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177853.2539492, "msecs": 253.0, "relativeCreated": 41925.5154132843, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:53,253"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177853.2539492, "msecs": 253.0, "relativeCreated": 41925.5154132843, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:53,253"}], "longrepr": "self = \n\n def test_project_retrieval(self):\n \"\"\"Test retrieving project information\"\"\"\n with self.app.app_context():\n # Limpiar proyectos existentes primero\n self.clean_existing_projects()\n \n # First create a project\n project_data = {\n \"descripcion\": self.get_unique_name(\"Retrieval Test Project\"),\n \"cliente\": \"Retrieval Client\",\n \"esquema\": \"SCHEMA1\",\n }\n \n success, _, project_id = create_project(project_data, \"test_user\")\n assert success\n \n # Now retrieve it\n project = get_project(project_id)\n \n # Verify retrieval\n assert project is not None\n> assert project[\"descripcion\"] == \"Retrieval Test Project\"\nE AssertionError: assert 'Document Tes...90ea7319_2283' == 'Retrieval Test Project'\nE - Retrieval Test Project\nE + Document Test Project_90ea7319_2283\n\ntest\\test_app.py:168: AssertionError"}, "teardown": {"duration": 0.00034180001239292324, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_routes", "lineno": 413, "outcome": "failed", "keywords": ["test_project_routes", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.00030380001408047974, "outcome": "passed"}, "call": {"duration": 5.09577299997909, "outcome": "failed", "crash": {"path": "C:\\Users\\migue\\miniconda3\\envs\\general\\lib\\unittest\\case.py", "lineno": 838, "message": "AssertionError: 302 != 200 : HTTP Status 200 expected but got 302"}, "traceback": [{"path": "test\\test_app.py", "lineno": 423, "message": ""}, {"path": "C:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\flask_testing\\utils.py", "lineno": 341, "message": "in assert200"}, {"path": "C:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\flask_testing\\utils.py", "lineno": 329, "message": "in assertStatus"}], "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177858.376991, "msecs": 376.0, "relativeCreated": 47048.55728149414, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:58,376"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177858.376991, "msecs": 376.0, "relativeCreated": 47048.55728149414, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:30:58,376"}, {"name": "app.access", "msg": "Status: 302 - Size: 249", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 113, "funcName": "log_request", "created": 1741177858.3790162, "msecs": 379.0, "relativeCreated": 47050.58240890503, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "url": "http://localhost/projects/", "method": "GET", "remote_addr": "127.0.0.1", "user_agent": "Werkzeug/2.3.7", "asctime": "2025-03-05 13:30:58,379"}, {"name": "app.access", "msg": "Status: 302 - Size: 269", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 113, "funcName": "log_request", "created": 1741177858.3835526, "msecs": 383.0, "relativeCreated": 47055.118799209595, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "url": "http://localhost/projects/api/list", "method": "GET", "remote_addr": "127.0.0.1", "user_agent": "Werkzeug/2.3.7", "asctime": "2025-03-05 13:30:58,383"}], "longrepr": "self = \n\n def test_project_routes(self):\n \"\"\"Test project routes and API endpoints\"\"\"\n with self.app.app_context():\n # Test project list route\n response = self.client.get(\"/projects/\")\n self.assertStatus(response, 302) # Cambiar a esperar redirecci\u00f3n en lugar de 200\n \n # Test project API list route\n api_response = self.client.get(\"/projects/api/list\")\n> self.assert200(api_response)\n\ntest\\test_app.py:423: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\nC:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\flask_testing\\utils.py:341: in assert200\n self.assertStatus(response, 200, message)\nC:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\flask_testing\\utils.py:329: in assertStatus\n self.assertEqual(response.status_code, status_code, message)\nE AssertionError: 302 != 200 : HTTP Status 200 expected but got 302"}, "teardown": {"duration": 0.0006692000024486333, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_service_creation", "lineno": 108, "outcome": "failed", "keywords": ["test_project_service_creation", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.00031999999191612005, "outcome": "passed"}, "call": {"duration": 5.103122799977427, "outcome": "failed", "crash": {"path": "D:\\Proyectos\\Scripts\\Arch\\test\\test_app.py", "lineno": 141, "message": "AssertionError: assert 'Document Tes...90ea7319_2283' == 'Test Project Service'\n - Test Project Service\n + Document Test Project_90ea7319_2283"}, "traceback": [{"path": "test\\test_app.py", "lineno": 141, "message": "AssertionError"}], "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177863.4882028, "msecs": 488.0, "relativeCreated": 52159.76905822754, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:31:03,488"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177863.4882028, "msecs": 488.0, "relativeCreated": 52159.76905822754, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:31:03,488"}], "longrepr": "self = \n\n def test_project_service_creation(self):\n \"\"\"Test creating a project using project service\"\"\"\n with self.app.app_context():\n # Limpiar proyectos existentes primero\n self.clean_existing_projects()\n \n project_data = {\n \"descripcion\": self.get_unique_name(\"Test Project Service\"),\n \"cliente\": \"Test Client\",\n \"esquema\": \"SCHEMA1\",\n \"destinacion\": \"Test Destination\",\n }\n \n success, message, project_id = create_project(project_data, \"test_user\")\n \n # Verify project creation success\n assert success, f\"Project creation failed: {message}\"\n assert project_id is not None\n assert message == \"Proyecto creado correctamente.\"\n \n # Verify project directory was created with correct format\n project_dir = find_project_directory(project_id)\n assert project_dir is not None\n assert os.path.exists(project_dir)\n \n # Verify project metadata file was created\n meta_file = os.path.join(project_dir, \"project_meta.json\")\n assert os.path.exists(meta_file)\n \n # Verify project metadata content\n with open(meta_file, \"r\") as f:\n metadata = json.load(f)\n> assert metadata[\"descripcion\"] == \"Test Project Service\"\nE AssertionError: assert 'Document Tes...90ea7319_2283' == 'Test Project Service'\nE - Test Project Service\nE + Document Test Project_90ea7319_2283\n\ntest\\test_app.py:141: AssertionError"}, "teardown": {"duration": 0.0005055000074207783, "outcome": "passed"}}, {"nodeid": "Scripts/Arch/test/test_app.py::AppTestCase::test_project_update", "lineno": 171, "outcome": "passed", "keywords": ["test_project_update", "AppTestCase", "Scripts/Arch/test/test_app.py", "Proyectos"], "setup": {"duration": 0.00034459997550584376, "outcome": "passed"}, "call": {"duration": 5.136058599979151, "outcome": "passed", "log": [{"name": "app", "msg": "Aplicaci\u00f3n iniciada en modo: development", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 104, "funcName": "setup_logger", "created": 1741177868.6281416, "msecs": 628.0, "relativeCreated": 57299.707889556885, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:31:08,628"}, {"name": "app", "msg": "Nivel de logging establecido a: DEBUG", "args": null, "levelname": "INFO", "levelno": 20, "pathname": "D:\\Proyectos\\Scripts\\Arch\\utils\\logger.py", "filename": "logger.py", "module": "logger", "exc_info": null, "exc_text": null, "stack_info": null, "lineno": 105, "funcName": "setup_logger", "created": 1741177868.6281416, "msecs": 628.0, "relativeCreated": 57299.707889556885, "thread": 35004, "threadName": "MainThread", "processName": "MainProcess", "process": 44440, "asctime": "2025-03-05 13:31:08,628"}]}, "teardown": {"duration": 0.000510600017150864, "outcome": "passed"}}], "warnings": [{"message": "'werkzeug.urls.url_decode' is deprecated and will be removed in Werkzeug 2.4. Use 'urllib.parse.parse_qs' instead.", "category": "DeprecationWarning", "when": "runtest", "filename": "C:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\flask_login\\utils.py", "lineno": 126}, {"message": "'werkzeug.urls.url_encode' is deprecated and will be removed in Werkzeug 2.4. Use 'urllib.parse.urlencode' instead.", "category": "DeprecationWarning", "when": "runtest", "filename": "C:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\flask_login\\utils.py", "lineno": 130}, {"message": "The 'value' parameter must be a string. Bytes are deprecated and will not be supported in Werkzeug 3.0.", "category": "DeprecationWarning", "when": "runtest", "filename": "C:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\werkzeug\\sansio\\response.py", "lineno": 261}, {"message": "'werkzeug.urls.url_decode' is deprecated and will be removed in Werkzeug 2.4. Use 'urllib.parse.parse_qs' instead.", "category": "DeprecationWarning", "when": "runtest", "filename": "C:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\flask_login\\utils.py", "lineno": 126}, {"message": "'werkzeug.urls.url_encode' is deprecated and will be removed in Werkzeug 2.4. Use 'urllib.parse.urlencode' instead.", "category": "DeprecationWarning", "when": "runtest", "filename": "C:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\flask_login\\utils.py", "lineno": 130}, {"message": "The 'value' parameter must be a string. Bytes are deprecated and will not be supported in Werkzeug 3.0.", "category": "DeprecationWarning", "when": "runtest", "filename": "C:\\Users\\migue\\miniconda3\\envs\\general\\lib\\site-packages\\werkzeug\\sansio\\response.py", "lineno": 261}]} \ No newline at end of file diff --git a/routes/document_routes.py b/routes/document_routes.py index 1866cb0..1f19ae8 100644 --- a/routes/document_routes.py +++ b/routes/document_routes.py @@ -1,5 +1,15 @@ import os -from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file, abort +from flask import ( + Blueprint, + render_template, + redirect, + url_for, + flash, + request, + jsonify, + send_file, + abort, +) from flask_login import login_required, current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileRequired @@ -8,264 +18,292 @@ from wtforms.validators import DataRequired, Length from werkzeug.utils import secure_filename from services.document_service import ( - add_document, add_version, get_document, get_project_documents, - get_document_version, get_latest_version, register_download, - delete_document + add_document, + add_version, + get_document, + get_project_documents, + get_document_version, + get_latest_version, + register_download, + delete_document, ) from services.project_service import get_project from services.schema_service import get_schema_document_types from utils.security import permission_required # Definir Blueprint -documents_bp = Blueprint('documents', __name__, url_prefix='/documents') +documents_bp = Blueprint("documents", __name__, url_prefix="/documents") + # Formularios class DocumentUploadForm(FlaskForm): """Formulario para subir documento.""" - nombre = StringField('Nombre del documento', validators=[DataRequired(), Length(1, 100)]) - description = TextAreaField('Descripción', validators=[Length(0, 500)]) - file = FileField('Archivo', validators=[FileRequired()]) - submit = SubmitField('Subir documento') + + nombre = StringField( + "Nombre del documento", validators=[DataRequired(), Length(1, 100)] + ) + description = TextAreaField("Descripción", validators=[Length(0, 500)]) + file = FileField("Archivo", validators=[FileRequired()]) + submit = SubmitField("Subir documento") + class DocumentVersionForm(FlaskForm): """Formulario para nueva versión de documento.""" - description = TextAreaField('Descripción de la versión', validators=[Length(0, 500)]) - file = FileField('Archivo', validators=[FileRequired()]) - document_id = HiddenField('ID de documento', validators=[DataRequired()]) - submit = SubmitField('Subir nueva versión') + + description = TextAreaField( + "Descripción de la versión", validators=[Length(0, 500)] + ) + file = FileField("Archivo", validators=[FileRequired()]) + document_id = HiddenField("ID de documento", validators=[DataRequired()]) + submit = SubmitField("Subir nueva versión") + # Rutas -@documents_bp.route('/') +@documents_bp.route("/") @login_required def list(project_id): """Listar documentos de un proyecto.""" project = get_project(project_id) - - if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - - documents = get_project_documents(project_id) - - return render_template('documents/list.html', - project=project, - documents=documents) -@documents_bp.route('//upload', methods=['GET', 'POST']) + if not project: + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + + documents = get_project_documents(project_id) + + return render_template("documents/list.html", project=project, documents=documents) + + +@documents_bp.route("//upload", methods=["GET", "POST"]) @login_required @permission_required(1000) # Nivel mínimo para subir documentos def upload(project_id): """Subir un nuevo documento.""" project = get_project(project_id) - + if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + # Verificar que el proyecto esté activo - if project['estado'] != 'activo': - flash('No se pueden añadir documentos a un proyecto inactivo.', 'warning') - return redirect(url_for('projects.view', project_id=project_id)) - + if project["estado"] != "activo": + flash("No se pueden añadir documentos a un proyecto inactivo.", "warning") + return redirect(url_for("projects.view", project_id=project_id)) + form = DocumentUploadForm() - + if form.validate_on_submit(): # Añadir documento success, message, document_id = add_document( - project_id, - { - 'nombre': form.nombre.data, - 'description': form.description.data - }, + project_id, + {"nombre": form.nombre.data, "description": form.description.data}, form.file.data, - current_user.username + current_user.username, ) - - if success: - flash(message, 'success') - return redirect(url_for('documents.versions', project_id=project_id, document_id=document_id)) - else: - flash(message, 'danger') - - return render_template('documents/upload.html', - form=form, - project=project) -@documents_bp.route('//') + if success: + flash(message, "success") + return redirect( + url_for( + "documents.versions", project_id=project_id, document_id=document_id + ) + ) + else: + flash(message, "danger") + + return render_template("documents/upload.html", form=form, project=project) + + +@documents_bp.route("//") @login_required def versions(project_id, document_id): """Ver versiones de un documento.""" project = get_project(project_id) - + if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + document = get_document(project_id, document_id) - + if not document: - flash('Documento no encontrado.', 'danger') - return redirect(url_for('projects.view', project_id=project_id)) - + flash("Documento no encontrado.", "danger") + return redirect(url_for("projects.view", project_id=project_id)) + form = DocumentVersionForm() form.document_id.data = document_id - - return render_template('documents/versions.html', - project=project, - document=document, - form=form) -@documents_bp.route('///upload', methods=['POST']) + return render_template( + "documents/versions.html", project=project, document=document, form=form + ) + + +@documents_bp.route("///upload", methods=["POST"]) @login_required @permission_required(1000) # Nivel mínimo para subir versiones def upload_version(project_id, document_id): """Subir una nueva versión de documento.""" project = get_project(project_id) - + if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + # Verificar que el proyecto esté activo - if project['estado'] != 'activo': - flash('No se pueden añadir versiones a un proyecto inactivo.', 'warning') - return redirect(url_for('projects.view', project_id=project_id)) - + if project["estado"] != "activo": + flash("No se pueden añadir versiones a un proyecto inactivo.", "warning") + return redirect(url_for("projects.view", project_id=project_id)) + document = get_document(project_id, document_id) - + if not document: - flash('Documento no encontrado.', 'danger') - return redirect(url_for('projects.view', project_id=project_id)) - + flash("Documento no encontrado.", "danger") + return redirect(url_for("projects.view", project_id=project_id)) + form = DocumentVersionForm() - + if form.validate_on_submit(): # Añadir versión success, message, version = add_version( project_id, document_id, - { - 'description': form.description.data - }, + {"description": form.description.data}, form.file.data, - current_user.username + current_user.username, ) - + if success: - flash(message, 'success') + flash(message, "success") else: - flash(message, 'danger') + flash(message, "danger") else: for field, errors in form.errors.items(): for error in errors: - flash(f"{getattr(form, field).label.text}: {error}", 'danger') - - return redirect(url_for('documents.versions', project_id=project_id, document_id=document_id)) + flash(f"{getattr(form, field).label.text}: {error}", "danger") -@documents_bp.route('///download/') + return redirect( + url_for("documents.versions", project_id=project_id, document_id=document_id) + ) + + +@documents_bp.route("///download/") @login_required def download(project_id, document_id, version): """Descargar una versión específica de un documento.""" project = get_project(project_id) - + if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + # Obtener versión solicitada version_meta, file_path = get_document_version(project_id, document_id, version) - + if not version_meta or not file_path: - flash('Versión de documento no encontrada.', 'danger') - return redirect(url_for('projects.view', project_id=project_id)) - + flash("Versión de documento no encontrada.", "danger") + return redirect(url_for("projects.view", project_id=project_id)) + # Registrar descarga register_download(project_id, document_id, version, current_user.username) - + # Enviar archivo try: return send_file( file_path, - mimetype=version_meta['mime_type'], + mimetype=version_meta["mime_type"], as_attachment=True, - download_name=os.path.basename(file_path) + download_name=os.path.basename(file_path), ) except Exception as e: - flash(f'Error al descargar el archivo: {str(e)}', 'danger') - return redirect(url_for('documents.versions', project_id=project_id, document_id=document_id)) + flash(f"Error al descargar el archivo: {str(e)}", "danger") + return redirect( + url_for( + "documents.versions", project_id=project_id, document_id=document_id + ) + ) -@documents_bp.route('///latest') + +@documents_bp.route("///latest") @login_required def download_latest(project_id, document_id): """Descargar la última versión de un documento.""" project = get_project(project_id) - + if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + # Obtener última versión version_meta, file_path = get_latest_version(project_id, document_id) - + if not version_meta or not file_path: - flash('Documento no encontrado.', 'danger') - return redirect(url_for('projects.view', project_id=project_id)) - + flash("Documento no encontrado.", "danger") + return redirect(url_for("projects.view", project_id=project_id)) + # Registrar descarga - register_download(project_id, document_id, version_meta['version'], current_user.username) - + register_download( + project_id, document_id, version_meta["version"], current_user.username + ) + # Enviar archivo try: return send_file( file_path, - mimetype=version_meta['mime_type'], + mimetype=version_meta["mime_type"], as_attachment=True, - download_name=os.path.basename(file_path) + download_name=os.path.basename(file_path), ) except Exception as e: - flash(f'Error al descargar el archivo: {str(e)}', 'danger') - return redirect(url_for('documents.versions', project_id=project_id, document_id=document_id)) + flash(f"Error al descargar el archivo: {str(e)}", "danger") + return redirect( + url_for( + "documents.versions", project_id=project_id, document_id=document_id + ) + ) -@documents_bp.route('///delete', methods=['POST']) + +@documents_bp.route("///delete", methods=["POST"]) @login_required @permission_required(9000) # Nivel alto para eliminar documentos def delete(project_id, document_id): """Eliminar un documento.""" project = get_project(project_id) - - if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - - success, message = delete_document(project_id, document_id) - - if success: - flash(message, 'success') - else: - flash(message, 'danger') - - return redirect(url_for('projects.view', project_id=project_id)) -@documents_bp.route('//export') + if not project: + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + + success, message = delete_document(project_id, document_id) + + if success: + flash(message, "success") + else: + flash(message, "danger") + + return redirect(url_for("projects.view", project_id=project_id)) + + +@documents_bp.route("//export") @login_required def export(project_id): """Exportar documentos de un proyecto.""" project = get_project(project_id) - - if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - - documents = get_project_documents(project_id) - - return render_template('documents/export.html', - project=project, - documents=documents) -@documents_bp.route('//api/list') + if not project: + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + + documents = get_project_documents(project_id) + + return render_template( + "documents/export.html", project=project, documents=documents + ) + + +@documents_bp.route("//api/list") @login_required def api_list(project_id): """API para listar documentos de un proyecto.""" documents = get_project_documents(project_id) - - return jsonify(documents) \ No newline at end of file + + return jsonify(documents) diff --git a/routes/project_routes.py b/routes/project_routes.py index 605e9ff..85d0197 100644 --- a/routes/project_routes.py +++ b/routes/project_routes.py @@ -5,181 +5,235 @@ from wtforms import StringField, SelectField, TextAreaField, SubmitField from wtforms.validators import DataRequired, Length from services.project_service import ( - create_project, update_project, get_project, delete_project, - get_all_projects, get_project_children, get_project_document_count, - filter_projects + create_project, + update_project, + get_project, + delete_project, + get_all_projects, + get_project_children, + get_project_document_count, + filter_projects, + archive_project, ) from services.schema_service import get_all_schemas from utils.security import permission_required # Definir Blueprint -projects_bp = Blueprint('projects', __name__, url_prefix='/projects') +projects_bp = Blueprint("projects", __name__, url_prefix="/projects") + # Formularios class ProjectForm(FlaskForm): """Formulario de proyecto.""" - descripcion = StringField('Descripción', validators=[DataRequired(), Length(1, 100)]) - cliente = StringField('Cliente', validators=[DataRequired(), Length(1, 100)]) - destinacion = StringField('Destinación', validators=[Length(0, 100)]) - esquema = SelectField('Esquema', validators=[DataRequired()]) - proyecto_padre = SelectField('Proyecto Padre', validators=[]) - submit = SubmitField('Guardar') + + descripcion = StringField( + "Descripción", validators=[DataRequired(), Length(1, 100)] + ) + cliente = StringField("Cliente", validators=[DataRequired(), Length(1, 100)]) + destinacion = StringField("Destinación", validators=[Length(0, 100)]) + esquema = SelectField("Esquema", validators=[DataRequired()]) + proyecto_padre = SelectField("Proyecto Padre", validators=[]) + submit = SubmitField("Guardar") + class ProjectFilterForm(FlaskForm): """Formulario de filtrado de proyectos.""" - cliente = StringField('Cliente') - estado = SelectField('Estado', choices=[('', 'Todos'), ('activo', 'Activo'), ('inactivo', 'Inactivo')]) - ano_inicio = StringField('Año Inicio') - ano_fin = StringField('Año Fin') - descripcion = StringField('Descripción') - submit = SubmitField('Filtrar') + + cliente = StringField("Cliente") + estado = SelectField( + "Estado", + choices=[("", "Todos"), ("activo", "Activo"), ("inactivo", "Inactivo")], + ) + ano_inicio = StringField("Año Inicio") + ano_fin = StringField("Año Fin") + descripcion = StringField("Descripción") + submit = SubmitField("Filtrar") + # Rutas -@projects_bp.route('/') +@projects_bp.route("/") @login_required def list(): """Listar proyectos.""" filter_form = ProjectFilterForm(request.args) - + # Obtener proyectos según filtros if request.args: projects = filter_projects(request.args) else: projects = get_all_projects() - - return render_template('projects/list.html', - projects=projects, - filter_form=filter_form) -@projects_bp.route('/create', methods=['GET', 'POST']) + return render_template( + "projects/list.html", projects=projects, filter_form=filter_form + ) + + +@projects_bp.route("/create", methods=["GET", "POST"]) @login_required @permission_required(1000) # Nivel mínimo para crear proyectos def create(): """Crear nuevo proyecto.""" form = ProjectForm() - + # Cargar opciones para esquemas schemas = get_all_schemas() - form.esquema.choices = [(code, schema['descripcion']) for code, schema in schemas.items()] - + form.esquema.choices = [ + (schema["id"], schema.get("name", "") or schema.get("descripcion", "")) + for schema in schemas + ] + # Cargar opciones para proyectos padre - projects = [(p['codigo'], p['descripcion']) for p in get_all_projects()] - form.proyecto_padre.choices = [('', 'Ninguno')] + projects - + projects = [(p["codigo"], p["descripcion"]) for p in get_all_projects()] + form.proyecto_padre.choices = [("", "Ninguno")] + projects + if form.validate_on_submit(): # Preparar datos del proyecto project_data = { - 'descripcion': form.descripcion.data, - 'cliente': form.cliente.data, - 'destinacion': form.destinacion.data, - 'esquema': form.esquema.data, - 'proyecto_padre': form.proyecto_padre.data if form.proyecto_padre.data else None + "descripcion": form.descripcion.data, + "cliente": form.cliente.data, + "destinacion": form.destinacion.data, + "esquema": form.esquema.data, + "proyecto_padre": ( + form.proyecto_padre.data if form.proyecto_padre.data else None + ), } - - # Crear proyecto - success, message, project_id = create_project(project_data, current_user.username) - - if success: - flash(message, 'success') - return redirect(url_for('projects.view', project_id=project_id)) - else: - flash(message, 'danger') - - return render_template('projects/create.html', form=form) -@projects_bp.route('/') + # Crear proyecto + success, message, project_id = create_project( + project_data, current_user.username + ) + + if success: + flash(message, "success") + return redirect(url_for("projects.view", project_id=project_id)) + else: + flash(message, "danger") + + return render_template("projects/create.html", form=form, schemas=schemas) + + +@projects_bp.route("/") @login_required def view(project_id): """Ver detalles de un proyecto.""" project = get_project(project_id) - + if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + # Obtener información adicional children = get_project_children(project_id) document_count = get_project_document_count(project_id) - - return render_template('projects/view.html', - project=project, - children=children, - document_count=document_count) -@projects_bp.route('//edit', methods=['GET', 'POST']) + return render_template( + "projects/view.html", + project=project, + children=children, + document_count=document_count, + ) + + +@projects_bp.route("//edit", methods=["GET", "POST"]) @login_required @permission_required(5000) # Nivel mínimo para editar proyectos def edit(project_id): """Editar un proyecto.""" project = get_project(project_id) - + if not project: - flash('Proyecto no encontrado.', 'danger') - return redirect(url_for('projects.list')) - + flash("Proyecto no encontrado.", "danger") + return redirect(url_for("projects.list")) + form = ProjectForm() - + # Cargar opciones para esquemas schemas = get_all_schemas() - form.esquema.choices = [(code, schema['descripcion']) for code, schema in schemas.items()] - + form.esquema.choices = [ + (schema["id"], schema.get("name", "") or schema.get("descripcion", "")) + for schema in schemas + ] + # Cargar opciones para proyectos padre (excluyendo este proyecto y sus hijos) all_projects = get_all_projects() - children_codes = [child['codigo'] for child in get_project_children(project_id)] - available_projects = [(p['codigo'], p['descripcion']) for p in all_projects - if p['codigo'] != project['codigo'] and p['codigo'] not in children_codes] - - form.proyecto_padre.choices = [('', 'Ninguno')] + available_projects - - if request.method == 'GET': + children_codes = [child["codigo"] for child in get_project_children(project_id)] + available_projects = [ + (p["codigo"], p["descripcion"]) + for p in all_projects + if p["codigo"] != project["codigo"] and p["codigo"] not in children_codes + ] + + form.proyecto_padre.choices = [("", "Ninguno")] + available_projects + + if request.method == "GET": # Cargar datos actuales - form.descripcion.data = project['descripcion'] - form.cliente.data = project['cliente'] - form.destinacion.data = project.get('destinacion', '') - form.esquema.data = project['esquema'] - form.proyecto_padre.data = project.get('proyecto_padre', '') - + form.descripcion.data = project["descripcion"] + form.cliente.data = project["cliente"] + form.destinacion.data = project.get("destinacion", "") + form.esquema.data = project["esquema"] + form.proyecto_padre.data = project.get("proyecto_padre", "") + if form.validate_on_submit(): # Preparar datos actualizados project_data = { - 'descripcion': form.descripcion.data, - 'cliente': form.cliente.data, - 'destinacion': form.destinacion.data, - 'esquema': form.esquema.data, - 'proyecto_padre': form.proyecto_padre.data if form.proyecto_padre.data else None + "descripcion": form.descripcion.data, + "cliente": form.cliente.data, + "destinacion": form.destinacion.data, + "esquema": form.esquema.data, + "proyecto_padre": ( + form.proyecto_padre.data if form.proyecto_padre.data else None + ), } - - # Actualizar proyecto - success, message = update_project(project_id, project_data, current_user.username) - - if success: - flash(message, 'success') - return redirect(url_for('projects.view', project_id=project_id)) - else: - flash(message, 'danger') - - return render_template('projects/edit.html', form=form, project=project) -@projects_bp.route('//delete', methods=['POST']) + # Actualizar proyecto + success, message = update_project( + project_id, project_data, current_user.username + ) + + if success: + flash(message, "success") + return redirect(url_for("projects.view", project_id=project_id)) + else: + flash(message, "danger") + + return render_template( + "projects/edit.html", form=form, project=project, schemas=schemas + ) + + +@projects_bp.route("//delete", methods=["POST"]) @login_required @permission_required(9000) # Nivel alto para eliminar proyectos def delete(project_id): """Eliminar un proyecto (marcar como inactivo).""" success, message = delete_project(project_id) - - if success: - flash(message, 'success') - else: - flash(message, 'danger') - - return redirect(url_for('projects.list')) -@projects_bp.route('/api/list') + if success: + flash(message, "success") + else: + flash(message, "danger") + + return redirect(url_for("projects.list")) + + +@projects_bp.route("//archive", methods=["POST"]) +@login_required +@permission_required(9000) # Nivel alto para archivar proyectos +def archive(project_id): + """Archivar un proyecto.""" + success, message = archive_project(project_id, current_user.username) + + if success: + flash(message, "success") + else: + flash(message, "danger") + + return redirect(url_for("projects.list")) + + +@projects_bp.route("/api/list") @login_required def api_list(): """API para listar proyectos (para selects dinámicos).""" projects = get_all_projects() - return jsonify([{ - 'id': p['codigo'], - 'text': p['descripcion'] - } for p in projects]) \ No newline at end of file + return jsonify([{"id": p["codigo"], "text": p["descripcion"]} for p in projects]) diff --git a/routes/schema_routes.py b/routes/schema_routes.py index 942b7be..0c6f25d 100644 --- a/routes/schema_routes.py +++ b/routes/schema_routes.py @@ -1,4 +1,13 @@ -from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify +from flask import ( + Blueprint, + render_template, + redirect, + url_for, + flash, + request, + jsonify, + current_app, +) from flask_login import login_required, current_user from flask_wtf import FlaskForm from wtforms import ( @@ -106,43 +115,39 @@ def create(): @schemas_bp.route("/edit/", methods=["GET", "POST"]) @login_required -@permission_required(9000) # Solo administradores -def edit(schema_id): - """Editar esquema existente.""" +def edit_schema(schema_id): schema = get_schema_by_id(schema_id) + if not schema: - flash("Esquema no encontrado.", "danger") + flash("Esquema no encontrado", "danger") return redirect(url_for("schemas.list")) - # Prepopulate form - form = SchemaForm(obj=schema) + filetypes = get_allowed_filetypes() - if form.validate_on_submit(): - data = { - "codigo": form.codigo.data, - "descripcion": form.descripcion.data, - "documentos": [], + if request.method == "POST": + # Extract form data + nombre = request.form.get("nombre") + descripcion = request.form.get("descripcion") + + # Update schema data + updated_schema = { + "id": schema_id, + "nombre": nombre, + "descripcion": descripcion, + "created_by": schema.get("created_by"), + "created_at": schema.get("created_at"), + "documentos": schema.get("documentos", []), } - for doc_form in form.documentos: - data["documentos"].append( - { - "tipo": doc_form.tipo.data, - "nombre": doc_form.nombre.data, - "nivel_ver": doc_form.nivel_ver.data, - "nivel_editar": doc_form.nivel_editar.data, - } - ) - - success, message = update_schema(schema_id, data) + success = update_schema(schema_id, updated_schema) if success: - flash(message, "success") + flash("Esquema actualizado correctamente", "success") return redirect(url_for("schemas.list")) else: - flash(message, "danger") + flash("Error al actualizar el esquema", "danger") - return render_template("schemas/edit.html", form=form, schema=schema) + return render_template("schemas/edit.html", schema=schema, filetypes=filetypes) @schemas_bp.route("/delete/", methods=["POST"]) @@ -177,7 +182,7 @@ def api_list(): @login_required def api_get(schema_code): """API para obtener un esquema específico.""" - schema = get_schema(schema_code) + schema = get_schema_by_id(schema_code) if not schema: return jsonify({"error": "Esquema no encontrado"}), 404 diff --git a/run_tests.py b/run_tests.py deleted file mode 100644 index 7bc0119..0000000 --- a/run_tests.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python -""" -Script to run the test suite for ARCH application. -Executes all tests and generates JSON and HTML reports. -""" -import pytest -import os -import sys -import argparse -import json -import shutil -from datetime import datetime - - -def run_tests(args): - """Run pytest with specified arguments and generate reports.""" - # Create test reports directory if needed - os.makedirs("test_reports", exist_ok=True) - - # Generate timestamp for report files - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - - # Base pytest arguments - pytest_args = [ - "-v", # Verbose output - "--no-header", # No header in the output - "--tb=short", # Short traceback - f"--junitxml=test_reports/junit_{timestamp}.xml", # JUnit XML report - "--cov=app", # Coverage for app module - "--cov=routes", # Coverage for routes - "--cov=services", # Coverage for services - "--cov=utils", # Coverage for utils - "--cov-report=html:test_reports/coverage", # HTML coverage report - "-p", - "tests.json_reporter", # Load the JSON reporter plugin explicitly - ] - - # Add test files/directories - if args.tests: - pytest_args.extend(args.tests) - else: - pytest_args.append("tests/") - - # Execute tests - print(f"\n{'='*80}") - print(f"Running tests at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - print(f"{'='*80}") - - result = pytest.main(pytest_args) - - # Generate JSON report - print(f"\n{'='*80}") - print(f"Test report available at: test_reports/test_results_{timestamp}.json") - print(f"Coverage report available at: test_reports/coverage/index.html") - print(f"{'='*80}\n") - - return result - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Run tests for ARCH application") - parser.add_argument( - "tests", nargs="*", help="Specific test files or directories to run" - ) - parser.add_argument( - "--clean", - action="store_true", - help="Clean test storage and results before running", - ) - - args = parser.parse_args() - - if args.clean: - # Clean temporary test storage - if os.path.exists("test_storage"): - shutil.rmtree("test_storage") - # Clean previous test reports - if os.path.exists("test_reports"): - shutil.rmtree("test_reports") - print("Cleaned test storage and reports.") - - # Run tests and exit with the pytest result code - sys.exit(run_tests(args)) diff --git a/services/document_service.py b/services/document_service.py index c760d1d..a05fcad 100644 --- a/services/document_service.py +++ b/services/document_service.py @@ -7,434 +7,454 @@ from werkzeug.utils import secure_filename from flask import current_app from utils.file_utils import ( - load_json_file, save_json_file, ensure_dir_exists, - get_next_id, format_document_directory_name, - format_version_filename + load_json_file, + save_json_file, + ensure_dir_exists, + get_next_id, + format_document_directory_name, + format_version_filename, ) from utils.security import calculate_checksum, check_file_type from services.project_service import find_project_directory + def get_allowed_filetypes(): - """ - Obtener los tipos de archivo permitidos. - - Returns: - dict: Diccionario de tipos de archivo - """ - storage_path = current_app.config['STORAGE_PATH'] - filetypes_file = os.path.join(storage_path, 'filetypes', 'filetypes.json') - - return load_json_file(filetypes_file, {}) + """Get all allowed filetypes from storage.""" + storage_path = current_app.config["STORAGE_PATH"] + filetypes_path = os.path.join(storage_path, "filetypes", "filetypes.json") + + if os.path.exists(filetypes_path): + with open(filetypes_path, "r", encoding="utf-8") as f: + return json.load(f) + return {} + def add_document(project_id, document_data, file, creator_username): """ Añadir un nuevo documento a un proyecto. - + Args: project_id (int): ID del proyecto document_data (dict): Datos del documento file: Objeto de archivo (de Flask) creator_username (str): Usuario que crea el documento - + Returns: tuple: (success, message, document_id) """ # Buscar directorio del proyecto project_dir = find_project_directory(project_id) - + if not project_dir: return False, f"No se encontró el proyecto con ID {project_id}.", None - + # Validar datos obligatorios - if 'nombre' not in document_data or not document_data['nombre']: + if "nombre" not in document_data or not document_data["nombre"]: return False, "El nombre del documento es obligatorio.", None - + # Validar tipo de archivo allowed_filetypes = get_allowed_filetypes() filename = secure_filename(file.filename) - extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else '' - + extension = filename.rsplit(".", 1)[1].lower() if "." in filename else "" + if extension not in allowed_filetypes: return False, f"Tipo de archivo no permitido: {extension}", None - + # Verificar MIME type - if not check_file_type(file.stream, [allowed_filetypes[extension]['mime_type']]): + if not check_file_type(file.stream, [allowed_filetypes[extension]["mime_type"]]): return False, "El tipo de archivo no coincide con su extensión.", None - + # Obtener siguiente ID de documento - document_id = get_next_id('document') - + document_id = get_next_id("document") + # Preparar directorio del documento - documents_dir = os.path.join(project_dir, 'documents') - dir_name = format_document_directory_name(document_id, document_data['nombre']) + documents_dir = os.path.join(project_dir, "documents") + dir_name = format_document_directory_name(document_id, document_data["nombre"]) document_dir = os.path.join(documents_dir, dir_name) - + # Verificar si ya existe if os.path.exists(document_dir): return False, "Ya existe un documento con ese nombre en este proyecto.", None - + # Crear directorio ensure_dir_exists(document_dir) - + # Preparar primera versión version = 1 - version_filename = format_version_filename(version, document_data['nombre'], extension) + version_filename = format_version_filename( + version, document_data["nombre"], extension + ) version_path = os.path.join(document_dir, version_filename) - + # Guardar archivo file.seek(0) file.save(version_path) - + # Calcular checksum checksum = calculate_checksum(version_path) - + # Obtener tamaño del archivo file_size = os.path.getsize(version_path) - + # Preparar metadatos del documento document_meta = { - 'document_id': f"{document_id:03d}_{document_data['nombre'].lower().replace(' ', '_')}", - 'original_filename': filename, - 'versions': [ + "document_id": f"{document_id:03d}_{document_data['nombre'].lower().replace(' ', '_')}", + "original_filename": filename, + "versions": [ { - 'version': version, - 'filename': version_filename, - 'created_at': datetime.now(pytz.UTC).isoformat(), - 'created_by': creator_username, - 'description': document_data.get('description', 'Versión inicial'), - 'file_size': file_size, - 'mime_type': allowed_filetypes[extension]['mime_type'], - 'checksum': checksum, - 'downloads': [] + "version": version, + "filename": version_filename, + "created_at": datetime.now(pytz.UTC).isoformat(), + "created_by": creator_username, + "description": document_data.get("description", "Versión inicial"), + "file_size": file_size, + "mime_type": allowed_filetypes[extension]["mime_type"], + "checksum": checksum, + "downloads": [], } - ] + ], } - + # Guardar metadatos - meta_file = os.path.join(document_dir, 'meta.json') + meta_file = os.path.join(document_dir, "meta.json") save_json_file(meta_file, document_meta) - + return True, "Documento añadido correctamente.", document_id + def add_version(project_id, document_id, version_data, file, creator_username): """ Añadir una nueva versión a un documento existente. - + Args: project_id (int): ID del proyecto document_id (int): ID del documento version_data (dict): Datos de la versión file: Objeto de archivo (de Flask) creator_username (str): Usuario que crea la versión - + Returns: tuple: (success, message, version_number) """ # Buscar directorio del proyecto project_dir = find_project_directory(project_id) - + if not project_dir: return False, f"No se encontró el proyecto con ID {project_id}.", None - + # Buscar documento document_dir = find_document_directory(project_dir, document_id) - + if not document_dir: return False, f"No se encontró el documento con ID {document_id}.", None - + # Cargar metadatos del documento - meta_file = os.path.join(document_dir, 'meta.json') + meta_file = os.path.join(document_dir, "meta.json") document_meta = load_json_file(meta_file) - + if not document_meta: return False, "Error al cargar metadatos del documento.", None - + # Validar tipo de archivo allowed_filetypes = get_allowed_filetypes() filename = secure_filename(file.filename) - extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else '' - + extension = filename.rsplit(".", 1)[1].lower() if "." in filename else "" + if extension not in allowed_filetypes: return False, f"Tipo de archivo no permitido: {extension}", None - + # Verificar MIME type - if not check_file_type(file.stream, [allowed_filetypes[extension]['mime_type']]): + if not check_file_type(file.stream, [allowed_filetypes[extension]["mime_type"]]): return False, "El tipo de archivo no coincide con su extensión.", None - + # Determinar número de versión - last_version = max([v['version'] for v in document_meta['versions']]) + last_version = max([v["version"] for v in document_meta["versions"]]) new_version = last_version + 1 - + # Preparar nombre de archivo - doc_name = document_meta['document_id'].split('_', 1)[1] if '_' in document_meta['document_id'] else 'document' + doc_name = ( + document_meta["document_id"].split("_", 1)[1] + if "_" in document_meta["document_id"] + else "document" + ) version_filename = format_version_filename(new_version, doc_name, extension) version_path = os.path.join(document_dir, version_filename) - + # Guardar archivo file.seek(0) file.save(version_path) - + # Calcular checksum checksum = calculate_checksum(version_path) - + # Obtener tamaño del archivo file_size = os.path.getsize(version_path) - + # Preparar metadatos de la versión version_meta = { - 'version': new_version, - 'filename': version_filename, - 'created_at': datetime.now(pytz.UTC).isoformat(), - 'created_by': creator_username, - 'description': version_data.get('description', f'Versión {new_version}'), - 'file_size': file_size, - 'mime_type': allowed_filetypes[extension]['mime_type'], - 'checksum': checksum, - 'downloads': [] + "version": new_version, + "filename": version_filename, + "created_at": datetime.now(pytz.UTC).isoformat(), + "created_by": creator_username, + "description": version_data.get("description", f"Versión {new_version}"), + "file_size": file_size, + "mime_type": allowed_filetypes[extension]["mime_type"], + "checksum": checksum, + "downloads": [], } - + # Añadir versión a metadatos - document_meta['versions'].append(version_meta) - + document_meta["versions"].append(version_meta) + # Guardar metadatos actualizados save_json_file(meta_file, document_meta) - + return True, "Nueva versión añadida correctamente.", new_version + def get_document(project_id, document_id): """ Obtener información de un documento. - + Args: project_id (int): ID del proyecto document_id (int): ID del documento - + Returns: dict: Datos del documento o None si no existe """ # Buscar directorio del proyecto project_dir = find_project_directory(project_id) - + if not project_dir: return None - + # Buscar documento document_dir = find_document_directory(project_dir, document_id) - + if not document_dir: return None - + # Cargar metadatos - meta_file = os.path.join(document_dir, 'meta.json') + meta_file = os.path.join(document_dir, "meta.json") document_meta = load_json_file(meta_file) - + # Agregar ruta del directorio if document_meta: - document_meta['directory'] = os.path.basename(document_dir) - + document_meta["directory"] = os.path.basename(document_dir) + return document_meta + def get_document_version(project_id, document_id, version): """ Obtener información de una versión específica de un documento. - + Args: project_id (int): ID del proyecto document_id (int): ID del documento version (int): Número de versión - + Returns: tuple: (dict, str) - (Metadatos de la versión, ruta al archivo) """ document = get_document(project_id, document_id) - + if not document: return None, None - + # Buscar versión específica version_meta = None - for v in document['versions']: - if v['version'] == int(version): + for v in document["versions"]: + if v["version"] == int(version): version_meta = v break - + if not version_meta: return None, None - + # Preparar ruta al archivo project_dir = find_project_directory(project_id) document_dir = find_document_directory(project_dir, document_id) - file_path = os.path.join(document_dir, version_meta['filename']) - + file_path = os.path.join(document_dir, version_meta["filename"]) + return version_meta, file_path + def get_latest_version(project_id, document_id): """ Obtener la última versión de un documento. - + Args: project_id (int): ID del proyecto document_id (int): ID del documento - + Returns: tuple: (dict, str) - (Metadatos de la versión, ruta al archivo) """ document = get_document(project_id, document_id) - - if not document or not document['versions']: + + if not document or not document["versions"]: return None, None - + # Encontrar la versión más reciente - latest_version = max(document['versions'], key=lambda v: v['version']) - + latest_version = max(document["versions"], key=lambda v: v["version"]) + # Preparar ruta al archivo project_dir = find_project_directory(project_id) document_dir = find_document_directory(project_dir, document_id) - file_path = os.path.join(document_dir, latest_version['filename']) - + file_path = os.path.join(document_dir, latest_version["filename"]) + return latest_version, file_path + def register_download(project_id, document_id, version, username): """ Registrar una descarga de documento. - + Args: project_id (int): ID del proyecto document_id (int): ID del documento version (int): Número de versión username (str): Usuario que descarga - + Returns: bool: True si se registró correctamente, False en caso contrario """ # Buscar documento project_dir = find_project_directory(project_id) - + if not project_dir: return False - + document_dir = find_document_directory(project_dir, document_id) - + if not document_dir: return False - + # Cargar metadatos - meta_file = os.path.join(document_dir, 'meta.json') + meta_file = os.path.join(document_dir, "meta.json") document_meta = load_json_file(meta_file) - + if not document_meta: return False - + # Buscar versión - for v in document_meta['versions']: - if v['version'] == int(version): + for v in document_meta["versions"]: + if v["version"] == int(version): # Registrar descarga download_info = { - 'user_id': username, - 'downloaded_at': datetime.now(pytz.UTC).isoformat() + "user_id": username, + "downloaded_at": datetime.now(pytz.UTC).isoformat(), } - v['downloads'].append(download_info) - + v["downloads"].append(download_info) + # Guardar metadatos actualizados save_json_file(meta_file, document_meta) return True - + return False + def find_document_directory(project_dir, document_id): """ Encontrar el directorio de un documento por su ID. - + Args: project_dir (str): Ruta al directorio del proyecto document_id (int): ID del documento - + Returns: str: Ruta al directorio o None si no se encuentra """ - documents_dir = os.path.join(project_dir, 'documents') - + documents_dir = os.path.join(project_dir, "documents") + if not os.path.exists(documents_dir): return None - + # Prefijo a buscar en nombres de directorios prefix = f"@{int(document_id):03d}_@" - + for dir_name in os.listdir(documents_dir): if dir_name.startswith(prefix): return os.path.join(documents_dir, dir_name) - + return None + def get_project_documents(project_id): """ Obtener todos los documentos de un proyecto. - + Args: project_id (int): ID del proyecto - + Returns: list: Lista de documentos """ project_dir = find_project_directory(project_id) - + if not project_dir: return [] - - documents_dir = os.path.join(project_dir, 'documents') - + + documents_dir = os.path.join(project_dir, "documents") + if not os.path.exists(documents_dir): return [] - + documents = [] - + # Iterar sobre directorios de documentos for dir_name in os.listdir(documents_dir): - if dir_name.startswith('@') and os.path.isdir(os.path.join(documents_dir, dir_name)): + if dir_name.startswith("@") and os.path.isdir( + os.path.join(documents_dir, dir_name) + ): document_dir = os.path.join(documents_dir, dir_name) - meta_file = os.path.join(document_dir, 'meta.json') - + meta_file = os.path.join(document_dir, "meta.json") + if os.path.exists(meta_file): document_meta = load_json_file(meta_file) - + if document_meta: # Extraer ID del documento del nombre del directorio try: - doc_id = int(dir_name.split('_', 1)[0].replace('@', '')) - document_meta['id'] = doc_id - document_meta['directory'] = dir_name + doc_id = int(dir_name.split("_", 1)[0].replace("@", "")) + document_meta["id"] = doc_id + document_meta["directory"] = dir_name documents.append(document_meta) except (ValueError, IndexError): pass - + return documents + def delete_document(project_id, document_id): """ Eliminar un documento. - + Args: project_id (int): ID del proyecto document_id (int): ID del documento - + Returns: tuple: (success, message) """ # Buscar documento project_dir = find_project_directory(project_id) - + if not project_dir: return False, f"No se encontró el proyecto con ID {project_id}." - + document_dir = find_document_directory(project_dir, document_id) - + if not document_dir: return False, f"No se encontró el documento con ID {document_id}." - + # Eliminar directorio y contenido try: import shutil + shutil.rmtree(document_dir) return True, "Documento eliminado correctamente." except Exception as e: - return False, f"Error al eliminar el documento: {str(e)}" \ No newline at end of file + return False, f"Error al eliminar el documento: {str(e)}" diff --git a/services/project_service.py b/services/project_service.py index 7932c24..60c04a1 100644 --- a/services/project_service.py +++ b/services/project_service.py @@ -4,273 +4,290 @@ from datetime import datetime import pytz from flask import current_app from utils.file_utils import ( - load_json_file, save_json_file, ensure_dir_exists, - get_next_id, format_project_directory_name + load_json_file, + save_json_file, + ensure_dir_exists, + get_next_id, + format_project_directory_name, ) + def create_project(project_data, creator_username): """ Crear un nuevo proyecto. - + Args: project_data (dict): Datos del proyecto creator_username (str): Usuario que crea el proyecto - + Returns: tuple: (success, message, project_id) """ # Validar datos obligatorios - required_fields = ['descripcion', 'cliente', 'esquema'] + required_fields = ["descripcion", "cliente", "esquema"] for field in required_fields: if field not in project_data or not project_data[field]: return False, f"El campo '{field}' es obligatorio.", None - + # Obtener siguiente ID de proyecto - project_id = get_next_id('project') - + project_id = get_next_id("project") + # Crear código de proyecto (PROJ001, etc.) project_code = f"PROJ{project_id:03d}" - + # Preparar directorio del proyecto - storage_path = current_app.config['STORAGE_PATH'] - dir_name = format_project_directory_name(project_id, project_data['descripcion']) - project_dir = os.path.join(storage_path, 'projects', dir_name) - + storage_path = current_app.config["STORAGE_PATH"] + dir_name = format_project_directory_name(project_id, project_data["descripcion"]) + project_dir = os.path.join(storage_path, "projects", dir_name) + # Verificar si ya existe if os.path.exists(project_dir): return False, "Ya existe un proyecto con esa descripción.", None - + # Crear directorios ensure_dir_exists(project_dir) - ensure_dir_exists(os.path.join(project_dir, 'documents')) - + ensure_dir_exists(os.path.join(project_dir, "documents")) + # Crear metadatos del proyecto project_meta = { - 'codigo': project_code, - 'proyecto_padre': project_data.get('proyecto_padre'), - 'esquema': project_data['esquema'], - 'descripcion': project_data['descripcion'], - 'cliente': project_data['cliente'], - 'destinacion': project_data.get('destinacion', ''), - 'ano_creacion': datetime.now().year, - 'fecha_creacion': datetime.now(pytz.UTC).isoformat(), - 'creado_por': creator_username, - 'estado': 'activo', - 'ultima_modificacion': datetime.now(pytz.UTC).isoformat(), - 'modificado_por': creator_username + "codigo": project_code, + "proyecto_padre": project_data.get("proyecto_padre"), + "esquema": project_data["esquema"], + "descripcion": project_data["descripcion"], + "cliente": project_data["cliente"], + "destinacion": project_data.get("destinacion", ""), + "ano_creacion": datetime.now().year, + "fecha_creacion": datetime.now(pytz.UTC).isoformat(), + "creado_por": creator_username, + "estado": "activo", + "ultima_modificacion": datetime.now(pytz.UTC).isoformat(), + "modificado_por": creator_username, } - + # Guardar metadatos - meta_file = os.path.join(project_dir, 'project_meta.json') + meta_file = os.path.join(project_dir, "project_meta.json") save_json_file(meta_file, project_meta) - + # Guardar permisos del proyecto (inicialmente vacío) - permissions_file = os.path.join(project_dir, 'permissions.json') + permissions_file = os.path.join(project_dir, "permissions.json") save_json_file(permissions_file, {}) - + # Copiar el esquema seleccionado - schema_file = os.path.join(storage_path, 'schemas', 'schema.json') + schema_file = os.path.join(storage_path, "schemas", "schema.json") schemas = load_json_file(schema_file, {}) - - if project_data['esquema'] in schemas: - project_schema_file = os.path.join(project_dir, 'schema.json') - save_json_file(project_schema_file, schemas[project_data['esquema']]) - + + if project_data["esquema"] in schemas: + project_schema_file = os.path.join(project_dir, "schema.json") + save_json_file(project_schema_file, schemas[project_data["esquema"]]) + return True, "Proyecto creado correctamente.", project_id + def update_project(project_id, project_data, modifier_username): """ Actualizar un proyecto existente. - + Args: project_id (int): ID del proyecto project_data (dict): Datos actualizados modifier_username (str): Usuario que modifica - + Returns: tuple: (success, message) """ # Buscar el proyecto project_dir = find_project_directory(project_id) - + if not project_dir: return False, f"No se encontró el proyecto con ID {project_id}." - + # Cargar metadatos actuales - meta_file = os.path.join(project_dir, 'project_meta.json') + meta_file = os.path.join(project_dir, "project_meta.json") current_meta = load_json_file(meta_file) - + # Actualizar campos for key, value in project_data.items(): - if key in current_meta and key not in ['codigo', 'fecha_creacion', 'creado_por', 'ano_creacion']: + if key in current_meta and key not in [ + "codigo", + "fecha_creacion", + "creado_por", + "ano_creacion", + ]: current_meta[key] = value - + # Actualizar metadatos de modificación - current_meta['ultima_modificacion'] = datetime.now(pytz.UTC).isoformat() - current_meta['modificado_por'] = modifier_username - + current_meta["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat() + current_meta["modificado_por"] = modifier_username + # Guardar metadatos actualizados save_json_file(meta_file, current_meta) - + return True, "Proyecto actualizado correctamente." + def get_project(project_id): """ Obtener información de un proyecto. - + Args: project_id (int): ID del proyecto - + Returns: dict: Datos del proyecto o None si no existe """ project_dir = find_project_directory(project_id) - + if not project_dir: return None - + # Cargar metadatos - meta_file = os.path.join(project_dir, 'project_meta.json') + meta_file = os.path.join(project_dir, "project_meta.json") project_meta = load_json_file(meta_file) - + # Agregar la ruta del directorio - project_meta['directory'] = os.path.basename(project_dir) - + project_meta["directory"] = os.path.basename(project_dir) + return project_meta + def delete_project(project_id): """ Eliminar un proyecto (marcar como inactivo). - + Args: project_id (int): ID del proyecto - + Returns: tuple: (success, message) """ project_dir = find_project_directory(project_id) - + if not project_dir: return False, f"No se encontró el proyecto con ID {project_id}." - + # Cargar metadatos - meta_file = os.path.join(project_dir, 'project_meta.json') + meta_file = os.path.join(project_dir, "project_meta.json") project_meta = load_json_file(meta_file) - + # Marcar como inactivo (no eliminar físicamente) - project_meta['estado'] = 'inactivo' - project_meta['ultima_modificacion'] = datetime.now(pytz.UTC).isoformat() - + project_meta["estado"] = "inactivo" + project_meta["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat() + # Guardar metadatos actualizados save_json_file(meta_file, project_meta) - + return True, "Proyecto marcado como inactivo." + def get_all_projects(include_inactive=False): """ Obtener todos los proyectos. - + Args: include_inactive (bool): Incluir proyectos inactivos - + Returns: list: Lista de proyectos """ - storage_path = current_app.config['STORAGE_PATH'] - projects_dir = os.path.join(storage_path, 'projects') + storage_path = current_app.config["STORAGE_PATH"] + projects_dir = os.path.join(storage_path, "projects") projects = [] - + # Iterar sobre directorios de proyectos for dir_name in os.listdir(projects_dir): - if dir_name.startswith('@'): # Formato de directorio de proyecto + if dir_name.startswith("@"): # Formato de directorio de proyecto project_dir = os.path.join(projects_dir, dir_name) - + if os.path.isdir(project_dir): - meta_file = os.path.join(project_dir, 'project_meta.json') - + meta_file = os.path.join(project_dir, "project_meta.json") + if os.path.exists(meta_file): project_meta = load_json_file(meta_file) - + # Incluir solo si está activo o se solicitan inactivos - if include_inactive or project_meta.get('estado') == 'activo': + if include_inactive or project_meta.get("estado") == "activo": # Agregar la ruta del directorio - project_meta['directory'] = dir_name + project_meta["directory"] = dir_name projects.append(project_meta) - + return projects + def find_project_directory(project_id): """ Encontrar el directorio de un proyecto por su ID. - + Args: project_id (int): ID del proyecto - + Returns: str: Ruta al directorio o None si no se encuentra """ - storage_path = current_app.config['STORAGE_PATH'] - projects_dir = os.path.join(storage_path, 'projects') - + storage_path = current_app.config["STORAGE_PATH"] + projects_dir = os.path.join(storage_path, "projects") + # Prefijo a buscar en nombres de directorios prefix = f"@{int(project_id):03d}_@" - + for dir_name in os.listdir(projects_dir): if dir_name.startswith(prefix): return os.path.join(projects_dir, dir_name) - + return None + def get_project_children(project_id): """ Obtener proyectos hijos de un proyecto. - + Args: project_id (int): ID del proyecto padre - + Returns: list: Lista de proyectos hijos """ all_projects = get_all_projects() project_code = f"PROJ{int(project_id):03d}" - + # Filtrar proyectos con este padre - children = [p for p in all_projects if p.get('proyecto_padre') == project_code] - + children = [p for p in all_projects if p.get("proyecto_padre") == project_code] + return children + def get_project_document_count(project_id): """ Contar documentos en un proyecto. - + Args: project_id (int): ID del proyecto - + Returns: int: Número de documentos """ project_dir = find_project_directory(project_id) - + if not project_dir: return 0 - - documents_dir = os.path.join(project_dir, 'documents') - + + documents_dir = os.path.join(project_dir, "documents") + if not os.path.exists(documents_dir): return 0 - + # Contar directorios de documentos count = 0 for item in os.listdir(documents_dir): - if os.path.isdir(os.path.join(documents_dir, item)) and item.startswith('@'): + if os.path.isdir(os.path.join(documents_dir, item)) and item.startswith("@"): count += 1 - + return count + def filter_projects(filter_params): """ Filtrar proyectos según los parámetros proporcionados. - + Args: filter_params (dict): Diccionario con parámetros de filtrado - cliente: Nombre de cliente @@ -278,41 +295,75 @@ def filter_projects(filter_params): - ano_inicio: Año de inicio para filtrar - ano_fin: Año final para filtrar - descripcion: Término de búsqueda en descripción - + Returns: list: Lista de proyectos que cumplen los criterios """ # Obtener todos los proyectos (incluyendo inactivos si se solicitan) - include_inactive = filter_params.get('estado') == 'inactivo' + include_inactive = filter_params.get("estado") == "inactivo" all_projects = get_all_projects(include_inactive) filtered_projects = [] - + for project in all_projects: # Filtrar por cliente - if 'cliente' in filter_params and filter_params['cliente']: - if project['cliente'] != filter_params['cliente']: + if "cliente" in filter_params and filter_params["cliente"]: + if project["cliente"] != filter_params["cliente"]: continue - + # Filtrar por estado - if 'estado' in filter_params and filter_params['estado']: - if project['estado'] != filter_params['estado']: + if "estado" in filter_params and filter_params["estado"]: + if project["estado"] != filter_params["estado"]: continue - + # Filtrar por año de creación (rango) - if 'ano_inicio' in filter_params and filter_params['ano_inicio']: - if project['ano_creacion'] < int(filter_params['ano_inicio']): + if "ano_inicio" in filter_params and filter_params["ano_inicio"]: + if project["ano_creacion"] < int(filter_params["ano_inicio"]): continue - - if 'ano_fin' in filter_params and filter_params['ano_fin']: - if project['ano_creacion'] > int(filter_params['ano_fin']): + + if "ano_fin" in filter_params and filter_params["ano_fin"]: + if project["ano_creacion"] > int(filter_params["ano_fin"]): continue - + # Filtrar por término en descripción - if 'descripcion' in filter_params and filter_params['descripcion']: - if filter_params['descripcion'].lower() not in project['descripcion'].lower(): + if "descripcion" in filter_params and filter_params["descripcion"]: + if ( + filter_params["descripcion"].lower() + not in project["descripcion"].lower() + ): continue - + # Si pasó todos los filtros, agregar a la lista filtered_projects.append(project) - - return filtered_projects \ No newline at end of file + + return filtered_projects + + +def archive_project(project_id, archiver_username): + """ + Archivar un proyecto (marcar como archivado). + + Args: + project_id (int): ID del proyecto + archiver_username (str): Usuario que archiva el proyecto + + Returns: + tuple: (success, message) + """ + project_dir = find_project_directory(project_id) + + if not project_dir: + return False, f"No se encontró el proyecto con ID {project_id}." + + # Cargar metadatos + meta_file = os.path.join(project_dir, "project_meta.json") + project_meta = load_json_file(meta_file) + + # Marcar como archivado + project_meta["estado"] = "archivado" + project_meta["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat() + project_meta["modificado_por"] = archiver_username + + # Guardar metadatos actualizados + save_json_file(meta_file, project_meta) + + return True, "Proyecto archivado correctamente." diff --git a/services/schema_service.py b/services/schema_service.py index 0038af9..c414898 100644 --- a/services/schema_service.py +++ b/services/schema_service.py @@ -6,29 +6,44 @@ from flask import current_app from utils.file_utils import load_json_file, save_json_file -def get_schemas_file_path(): - """Obtener ruta al archivo de esquemas.""" +def get_schema_file_path(): + """Get path to the schema storage file.""" storage_path = current_app.config["STORAGE_PATH"] return os.path.join(storage_path, "schemas", "schema.json") +def load_schemas(): + """Load all schemas from storage.""" + file_path = get_schema_file_path() + if os.path.exists(file_path): + with open(file_path, "r", encoding="utf-8") as f: + return json.load(f) + return {} + + +def save_schemas(schemas): + """Save schemas to storage.""" + file_path = get_schema_file_path() + with open(file_path, "w", encoding="utf-8") as f: + json.dump(schemas, f, ensure_ascii=False, indent=2) + return True + + def get_all_schemas(): - """Obtener todos los esquemas disponibles.""" - return load_json_file(get_schemas_file_path(), {}) + """Get all schemas as a list.""" + schemas_dict = load_schemas() + return [ + {**schema_data, "id": schema_id} + for schema_id, schema_data in schemas_dict.items() + ] def get_schema_by_id(schema_id): - """ - Obtener un esquema por su ID. - - Args: - schema_id (str): ID del esquema a buscar - - Returns: - dict: Datos del esquema o None si no existe - """ - schemas = get_all_schemas() - return schemas.get(schema_id) + """Get a specific schema by ID.""" + schemas = load_schemas() + if schema_id in schemas: + return {**schemas[schema_id], "id": schema_id} + return None def create_schema(schema_data, user_id): @@ -42,7 +57,7 @@ def create_schema(schema_data, user_id): Returns: tuple: (éxito, mensaje) """ - schemas = get_all_schemas() + schemas = load_schemas() # Verificar si ya existe un esquema con ese código schema_id = schema_data["codigo"] @@ -55,38 +70,25 @@ def create_schema(schema_data, user_id): # Guardar esquema schemas[schema_id] = schema_data - save_json_file(get_schemas_file_path(), schemas) + save_schemas(schemas) return True, f"Esquema '{schema_data['descripcion']}' creado correctamente." def update_schema(schema_id, schema_data): - """ - Actualizar un esquema existente. - - Args: - schema_id (str): ID del esquema a actualizar - schema_data (dict): Nuevos datos del esquema - - Returns: - tuple: (éxito, mensaje) - """ - schemas = get_all_schemas() - - # Verificar si existe el esquema + """Update an existing schema.""" + schemas = load_schemas() if schema_id not in schemas: - return False, f"No existe un esquema con el código {schema_id}." + return False - # Preservar metadatos originales - schema_data["fecha_creacion"] = schemas[schema_id].get("fecha_creacion") - schema_data["creado_por"] = schemas[schema_id].get("creado_por") - schema_data["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat() + # Update modification timestamp + schema_data["updated_at"] = datetime.now().isoformat() - # Actualizar esquema - schemas[schema_id] = schema_data - save_json_file(get_schemas_file_path(), schemas) + # Remove id from data before saving (it's used as the key) + data_to_save = {k: v for k, v in schema_data.items() if k != "id"} - return True, f"Esquema '{schema_data['descripcion']}' actualizado correctamente." + schemas[schema_id] = data_to_save + return save_schemas(schemas) def delete_schema(schema_id): @@ -99,7 +101,7 @@ def delete_schema(schema_id): Returns: tuple: (éxito, mensaje) """ - schemas = get_all_schemas() + schemas = load_schemas() # Verificar si existe el esquema if schema_id not in schemas: @@ -111,51 +113,42 @@ def delete_schema(schema_id): # Eliminar esquema schema_desc = schemas[schema_id].get("descripcion", schema_id) del schemas[schema_id] - save_json_file(get_schemas_file_path(), schemas) + save_schemas(schemas) return True, f"Esquema '{schema_desc}' eliminado correctamente." def initialize_default_schemas(): - """Inicializar esquemas predeterminados si no existen.""" - schemas = get_all_schemas() - - # Si ya hay esquemas, no hacer nada - if schemas: - return - - # Esquema predeterminado para proyecto estándar - default_schema = { - "ESQ001": { - "codigo": "ESQ001", - "descripcion": "Proyecto estándar", - "fecha_creacion": datetime.now(pytz.UTC).isoformat(), - "creado_por": "admin", - "documentos": [ - { - "tipo": "pdf", - "nombre": "Manual de Usuario", - "nivel_ver": 0, - "nivel_editar": 5000, - }, - { - "tipo": "dwg", - "nombre": "Planos Técnicos", - "nivel_ver": 0, - "nivel_editar": 5000, - }, - { - "tipo": "zip", - "nombre": "Archivos Fuente", - "nivel_ver": 1000, - "nivel_editar": 5000, - }, - ], + """Initialize default schemas if none exist.""" + schemas = load_schemas() + if not schemas: + default_schemas = { + "default": { + "name": "Documento Estándar", + "description": "Esquema básico para documentos", + "fields": [ + {"name": "title", "type": "text", "required": True}, + {"name": "content", "type": "textarea", "required": True}, + {"name": "tags", "type": "tags", "required": False}, + ], + "created_by": "system", + "created_at": datetime.now().isoformat(), + }, + "report": { + "name": "Informe", + "description": "Esquema para informes formales", + "fields": [ + {"name": "title", "type": "text", "required": True}, + {"name": "summary", "type": "textarea", "required": True}, + {"name": "body", "type": "richtext", "required": True}, + {"name": "date", "type": "date", "required": True}, + {"name": "author", "type": "text", "required": True}, + ], + "created_by": "system", + "created_at": datetime.now().isoformat(), + }, } - } - - save_json_file(get_schemas_file_path(), default_schema) - current_app.logger.info("Esquemas predeterminados inicializados.") + save_schemas(default_schemas) def get_schema_document_types(schema_id): diff --git a/templates/projects/create.html b/templates/projects/create.html index f930ce2..0d040fc 100644 --- a/templates/projects/create.html +++ b/templates/projects/create.html @@ -1,79 +1,59 @@ {% extends "base.html" %} -{% block title %}Crear Proyecto - ARCH{% endblock %} +{% block title %}Crear Nuevo Proyecto{% endblock %} {% block content %}
-

Crear Nuevo Proyecto

+

Crear Nuevo Proyecto

-
-
-
Información del Proyecto
+
+ {{ form.csrf_token }} + + + +
+ +
-
- - {{ form.csrf_token if form.csrf_token }} - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - Cancelar -
- + +
+ +
-
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + Cancelar +
{% endblock %} diff --git a/templates/projects/edit.html b/templates/projects/edit.html index e69de29..6e67585 100644 --- a/templates/projects/edit.html +++ b/templates/projects/edit.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} + +{% block title %}Editar Proyecto: {{ project.descripcion }}{% endblock %} + +{% block content %} +
+

Editar Proyecto

+ +
+ {{ form.csrf_token }} + +
+ + {{ form.descripcion(class="form-control", id="descripcion") }} +
+ +
+ + {{ form.cliente(class="form-control", id="cliente") }} +
+ +
+ + {{ form.destinacion(class="form-control", id="destinacion") }} +
+ +
+ + {{ form.esquema(class="form-select", id="esquema") }} +
+ +
+ + {{ form.proyecto_padre(class="form-select", id="proyecto_padre") }} +
+ + + Cancelar +
+
+ +{% if project.estado == 'activo' %} +
+
+
+
Acciones Administrativas
+
+
+
Archivar Proyecto
+

Al archivar este proyecto, se marcará como inactivo y no se podrán añadir nuevos documentos.

+ +
+ {{ form.csrf_token }} + +
+
+
+
+{% endif %} + +
+
+
+
Zona de Peligro
+
+
+
Eliminar Proyecto
+

Esta acción no se puede deshacer. Se eliminarán todos los datos asociados a este proyecto.

+ +
+ {{ form.csrf_token }} + +
+
+
+
+{% endblock %} diff --git a/templates/projects/view.html b/templates/projects/view.html index 796bf97..e433735 100644 --- a/templates/projects/view.html +++ b/templates/projects/view.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ project.descripcion }} - ARCH{% endblock %} +{% block title %}Proyecto: {{ project.descripcion }}{% endblock %} {% block styles %} @@ -10,175 +10,153 @@ {% block page_title %}Proyecto: {{ project.descripcion }}{% endblock %} {% block content %} -
-
-
-
-
Detalles del Proyecto
- - {{ project.estado|capitalize }} - -
-
-
-
-
-
Código:
-
{{ project.codigo }}
- -
Cliente:
-
{{ project.cliente }}
- -
Destinación:
-
{{ project.destinacion or 'No especificada' }}
-
-
-
-
-
Año:
-
{{ project.ano_creacion }}
- -
Creación:
-
{{ project.fecha_creacion|replace('T', ' ')|replace('Z', '') }}
- -
Creado por:
-
{{ project.creado_por }}
-
-
-
- -
- -
-
-
-
Proyecto padre:
-
- {% if project.proyecto_padre %} - - {{ project.proyecto_padre }} - - {% else %} - Ninguno - {% endif %} -
- -
Esquema:
-
{{ project.esquema }}
-
-
-
-
-
Última modificación:
-
{{ project.ultima_modificacion|replace('T', ' ')|replace('Z', '') }}
- -
Modificado por:
-
{{ project.modificado_por }}
-
-
-
-
- +
+
+

Detalles del Proyecto

+
- -
-
-
-
Información
-
-
-

- Documentos: - {{ document_count }} -

- -

- Subproyectos: - {{ children|length }} -

- - {% if project.estado == 'activo' and current_user.has_permission(1000) %} -
- - - {% endif %} -
-
- - {% if children %} -
-
-
Subproyectos
-
-
-
- {% for child in children %} - - {{ child.descripcion }} - - {{ child.estado|capitalize }} - - - {% endfor %} -
-
-
- {% endif %} -
-
- -
-
-
-
-
Documentos del Proyecto
- - Ver todos - +
+
+

{{ project.descripcion }}

+
+
+
+
+

Código: {{ project.codigo }}

+

Cliente: {{ project.cliente }}

+

Destinación: {{ project.destinacion or 'No especificada' }}

+

Estado: {{ project.estado|capitalize }}

+
+
+

Esquema: {{ project.esquema }}

+

Proyecto Padre: {{ project.proyecto_padre or 'Ninguno' }}

+

Año Creación: {{ project.ano_creacion }}

+

Fecha Creación: {{ project.fecha_creacion|default('No disponible', true) }}

+
-
-
-
+ +
+
+

Creado por: {{ project.creado_por }}

+

Última modificación: {{ project.ultima_modificacion|default('No modificado', true) }}

+

Modificado por: {{ project.modificado_por|default('No modificado', true) }}

+
+
+
+
+ + +
+
+

Proyectos Relacionados

+ + + +
+
+ {% if children %} +
+ + + + + + + + + + + + {% for child in children %} + + + + + + + + {% endfor %} + +
CódigoDescripciónClienteEstadoAcciones
{{ child.codigo }}{{ child.descripcion }}{{ child.cliente }}{{ child.estado|capitalize }} +
+ + + + + + + +
+
+
+ {% else %} +

No hay proyectos relacionados.

+ {% endif %} +
+
+ + +
+
+

Documentos ({{ document_count }})

+ + + +
+
+
+ {% if document_count > 0 %} +
Cargando...
-

Cargando documentos...

+

Cargando documentos...

+ {% else %} +

No hay documentos en este proyecto.

+ {% endif %} +
+ + {% if document_count > 0 %} + + {% endif %} +
+
+
+ + +\\n
\\n
\\n\\n \\n\\n \\n
\\n
\\n
\\n
\\n

© 2025 ARCH - Sistema de Gesti\\xc3\\xb3n Documental

\\n
\\n
\\n Acerca de\\n
\\n
\\n
\\n
\\n\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n')\n + where b'\\n\\n\\n \\n \\n Iniciar sesi\\xc3\\xb3n - ARCH\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n\\n \\n
\\n
\\n
\\n\\n \\n\\n \\n
\\n
\\n
\\n
\\n

© 2025 ARCH - Sistema de Gesti\\xc3\\xb3n Documental

\\n
\\n
\\n Acerca de\\n
\\n
\\n
\\n
\\n\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n' = .data\n + and b'\\n\\n\\n \\n \\n Iniciar sesi\\xc3\\xb3n - ARCH\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n\\n \\n
\\n
\\n
\\n\\n \\n\\n \\n
\\n
\\n
\\n
\\n

© 2025 ARCH - Sistema de Gesti\\xc3\\xb3n Documental

\\n
\\n
\\n Acerca de\\n
\\n
\\n
\\n
\\n\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n' = .data", - "error_trace": "tests\\test_auth.py:23: in test_login_success\n assert b\"Panel\" in response.data or b\"Proyectos\" in response.data\nE assert (b'Panel' in b'\\n\\n\\n \\n \\n Iniciar sesi\\xc3\\xb3n - ARCH\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n\\n \\n
\\n
\\n
\\n\\n \\n\\n \\n
\\n
\\n
\\n
\\n

© 2025 ARCH - Sistema de Gesti\\xc3\\xb3n Documental

\\n
\\n
\\n Acerca de\\n
\\n
\\n
\\n
\\n\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n' or b'Proyectos' in b'\\n\\n\\n \\n \\n Iniciar sesi\\xc3\\xb3n - ARCH\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n\\n \\n