Segunda version - Agregado Nombre al eschema

This commit is contained in:
Miguel 2025-03-05 18:10:33 +01:00
parent 9eb2bd5648
commit f31e4eb886
82 changed files with 2246 additions and 2684 deletions

16
app.py
View File

@ -23,13 +23,25 @@ cache = Cache()
def create_app(config_name=None): def create_app(config_name=None):
"""Fábrica de aplicación Flask.""" """Fábrica de aplicación Flask."""
app = Flask(__name__)
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: if config_name is None:
config_name = os.environ.get("FLASK_ENV", "default") config_name = os.environ.get("FLASK_ENV", "default")
app = Flask(__name__)
app.config.from_object(config[config_name]) app.config.from_object(config[config_name])
config[config_name].init_app(app) 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 # Inicializar extensiones
login_manager.init_app(app) login_manager.init_app(app)
bcrypt.init_app(app) bcrypt.init_app(app)

View File

@ -103,7 +103,8 @@ La aplicación sigue una arquitectura basada en archivos JSON y sistema de fiche
{ {
"ESQ001": { "ESQ001": {
"codigo": "ESQ001", "codigo": "ESQ001",
"descripcion": "Proyecto estándar", "nombre": "Siemens",
"descripcion": "Proyecto estándar Siemens",
"fecha_creacion": "2023-05-10T10:00:00Z", "fecha_creacion": "2023-05-10T10:00:00Z",
"creado_por": "admin", "creado_por": "admin",
"documentos": [ "documentos": [
@ -553,130 +554,6 @@ Se implementará un plugin personalizado para pytest que generará reportes en f
- Cobertura de código - Cobertura de código
- Detalles de errores encontrados - 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: 2. Instalar dependencias:
```bash ```bash
@ -747,6 +624,7 @@ filetypes = {
schemas = { schemas = {
"ESQ001": { "ESQ001": {
"codigo": "ESQ001", "codigo": "ESQ001",
"nombre": "Siemens",
"descripcion": "Proyecto estándar", "descripcion": "Proyecto estándar",
"fecha_creacion": "2023-05-10T10:00:00Z", "fecha_creacion": "2023-05-10T10:00:00Z",
"creado_por": "admin", "creado_por": "admin",

View File

@ -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())

View File

@ -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. Este proyecto está licenciado bajo los términos de la licencia MIT.
## Running Tests
To run the test suite:
```
Credenciales de acceso por defecto: Credenciales de acceso por defecto:
Usuario: admin Usuario: admin

1
resultados_test.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,15 @@
import os 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_login import login_required, current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired from flask_wtf.file import FileField, FileRequired
@ -8,50 +18,63 @@ from wtforms.validators import DataRequired, Length
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from services.document_service import ( from services.document_service import (
add_document, add_version, get_document, get_project_documents, add_document,
get_document_version, get_latest_version, register_download, add_version,
delete_document get_document,
get_project_documents,
get_document_version,
get_latest_version,
register_download,
delete_document,
) )
from services.project_service import get_project from services.project_service import get_project
from services.schema_service import get_schema_document_types from services.schema_service import get_schema_document_types
from utils.security import permission_required from utils.security import permission_required
# Definir Blueprint # Definir Blueprint
documents_bp = Blueprint('documents', __name__, url_prefix='/documents') documents_bp = Blueprint("documents", __name__, url_prefix="/documents")
# Formularios # Formularios
class DocumentUploadForm(FlaskForm): class DocumentUploadForm(FlaskForm):
"""Formulario para subir documento.""" """Formulario para subir documento."""
nombre = StringField('Nombre del documento', validators=[DataRequired(), Length(1, 100)])
description = TextAreaField('Descripción', validators=[Length(0, 500)]) nombre = StringField(
file = FileField('Archivo', validators=[FileRequired()]) "Nombre del documento", validators=[DataRequired(), Length(1, 100)]
submit = SubmitField('Subir documento') )
description = TextAreaField("Descripción", validators=[Length(0, 500)])
file = FileField("Archivo", validators=[FileRequired()])
submit = SubmitField("Subir documento")
class DocumentVersionForm(FlaskForm): class DocumentVersionForm(FlaskForm):
"""Formulario para nueva versión de documento.""" """Formulario para nueva versión de documento."""
description = TextAreaField('Descripción de la versión', validators=[Length(0, 500)])
file = FileField('Archivo', validators=[FileRequired()]) description = TextAreaField(
document_id = HiddenField('ID de documento', validators=[DataRequired()]) "Descripción de la versión", validators=[Length(0, 500)]
submit = SubmitField('Subir nueva versión') )
file = FileField("Archivo", validators=[FileRequired()])
document_id = HiddenField("ID de documento", validators=[DataRequired()])
submit = SubmitField("Subir nueva versión")
# Rutas # Rutas
@documents_bp.route('/<int:project_id>') @documents_bp.route("/<project_id>")
@login_required @login_required
def list(project_id): def list(project_id):
"""Listar documentos de un proyecto.""" """Listar documentos de un proyecto."""
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
documents = get_project_documents(project_id) documents = get_project_documents(project_id)
return render_template('documents/list.html', return render_template("documents/list.html", project=project, documents=documents)
project=project,
documents=documents)
@documents_bp.route('/<int:project_id>/upload', methods=['GET', 'POST'])
@documents_bp.route("/<project_id>/upload", methods=["GET", "POST"])
@login_required @login_required
@permission_required(1000) # Nivel mínimo para subir documentos @permission_required(1000) # Nivel mínimo para subir documentos
def upload(project_id): def upload(project_id):
@ -59,13 +82,13 @@ def upload(project_id):
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
# Verificar que el proyecto esté activo # Verificar que el proyecto esté activo
if project['estado'] != 'activo': if project["estado"] != "activo":
flash('No se pueden añadir documentos a un proyecto inactivo.', 'warning') flash("No se pueden añadir documentos a un proyecto inactivo.", "warning")
return redirect(url_for('projects.view', project_id=project_id)) return redirect(url_for("projects.view", project_id=project_id))
form = DocumentUploadForm() form = DocumentUploadForm()
@ -73,49 +96,49 @@ def upload(project_id):
# Añadir documento # Añadir documento
success, message, document_id = add_document( success, message, document_id = add_document(
project_id, project_id,
{ {"nombre": form.nombre.data, "description": form.description.data},
'nombre': form.nombre.data,
'description': form.description.data
},
form.file.data, form.file.data,
current_user.username current_user.username,
) )
if success: if success:
flash(message, 'success') flash(message, "success")
return redirect(url_for('documents.versions', project_id=project_id, document_id=document_id)) return redirect(
url_for(
"documents.versions", project_id=project_id, document_id=document_id
)
)
else: else:
flash(message, 'danger') flash(message, "danger")
return render_template('documents/upload.html', return render_template("documents/upload.html", form=form, project=project)
form=form,
project=project)
@documents_bp.route('/<int:project_id>/<int:document_id>')
@documents_bp.route("/<project_id>/<int:document_id>")
@login_required @login_required
def versions(project_id, document_id): def versions(project_id, document_id):
"""Ver versiones de un documento.""" """Ver versiones de un documento."""
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
document = get_document(project_id, document_id) document = get_document(project_id, document_id)
if not document: if not document:
flash('Documento no encontrado.', 'danger') flash("Documento no encontrado.", "danger")
return redirect(url_for('projects.view', project_id=project_id)) return redirect(url_for("projects.view", project_id=project_id))
form = DocumentVersionForm() form = DocumentVersionForm()
form.document_id.data = document_id form.document_id.data = document_id
return render_template('documents/versions.html', return render_template(
project=project, "documents/versions.html", project=project, document=document, form=form
document=document, )
form=form)
@documents_bp.route('/<int:project_id>/<int:document_id>/upload', methods=['POST'])
@documents_bp.route("/<project_id>/<int:document_id>/upload", methods=["POST"])
@login_required @login_required
@permission_required(1000) # Nivel mínimo para subir versiones @permission_required(1000) # Nivel mínimo para subir versiones
def upload_version(project_id, document_id): def upload_version(project_id, document_id):
@ -123,19 +146,19 @@ def upload_version(project_id, document_id):
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
# Verificar que el proyecto esté activo # Verificar que el proyecto esté activo
if project['estado'] != 'activo': if project["estado"] != "activo":
flash('No se pueden añadir versiones a un proyecto inactivo.', 'warning') flash("No se pueden añadir versiones a un proyecto inactivo.", "warning")
return redirect(url_for('projects.view', project_id=project_id)) return redirect(url_for("projects.view", project_id=project_id))
document = get_document(project_id, document_id) document = get_document(project_id, document_id)
if not document: if not document:
flash('Documento no encontrado.', 'danger') flash("Documento no encontrado.", "danger")
return redirect(url_for('projects.view', project_id=project_id)) return redirect(url_for("projects.view", project_id=project_id))
form = DocumentVersionForm() form = DocumentVersionForm()
@ -144,40 +167,41 @@ def upload_version(project_id, document_id):
success, message, version = add_version( success, message, version = add_version(
project_id, project_id,
document_id, document_id,
{ {"description": form.description.data},
'description': form.description.data
},
form.file.data, form.file.data,
current_user.username current_user.username,
) )
if success: if success:
flash(message, 'success') flash(message, "success")
else: else:
flash(message, 'danger') flash(message, "danger")
else: else:
for field, errors in form.errors.items(): for field, errors in form.errors.items():
for error in errors: for error in errors:
flash(f"{getattr(form, field).label.text}: {error}", 'danger') flash(f"{getattr(form, field).label.text}: {error}", "danger")
return redirect(url_for('documents.versions', project_id=project_id, document_id=document_id)) return redirect(
url_for("documents.versions", project_id=project_id, document_id=document_id)
)
@documents_bp.route('/<int:project_id>/<int:document_id>/download/<int:version>')
@documents_bp.route("/<project_id>/<int:document_id>/download/<int:version>")
@login_required @login_required
def download(project_id, document_id, version): def download(project_id, document_id, version):
"""Descargar una versión específica de un documento.""" """Descargar una versión específica de un documento."""
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
# Obtener versión solicitada # Obtener versión solicitada
version_meta, file_path = get_document_version(project_id, document_id, version) version_meta, file_path = get_document_version(project_id, document_id, version)
if not version_meta or not file_path: if not version_meta or not file_path:
flash('Versión de documento no encontrada.', 'danger') flash("Versión de documento no encontrada.", "danger")
return redirect(url_for('projects.view', project_id=project_id)) return redirect(url_for("projects.view", project_id=project_id))
# Registrar descarga # Registrar descarga
register_download(project_id, document_id, version, current_user.username) register_download(project_id, document_id, version, current_user.username)
@ -186,47 +210,59 @@ def download(project_id, document_id, version):
try: try:
return send_file( return send_file(
file_path, file_path,
mimetype=version_meta['mime_type'], mimetype=version_meta["mime_type"],
as_attachment=True, as_attachment=True,
download_name=os.path.basename(file_path) download_name=os.path.basename(file_path),
) )
except Exception as e: except Exception as e:
flash(f'Error al descargar el archivo: {str(e)}', 'danger') flash(f"Error al descargar el archivo: {str(e)}", "danger")
return redirect(url_for('documents.versions', project_id=project_id, document_id=document_id)) return redirect(
url_for(
"documents.versions", project_id=project_id, document_id=document_id
)
)
@documents_bp.route('/<int:project_id>/<int:document_id>/latest')
@documents_bp.route("/<project_id>/<int:document_id>/latest")
@login_required @login_required
def download_latest(project_id, document_id): def download_latest(project_id, document_id):
"""Descargar la última versión de un documento.""" """Descargar la última versión de un documento."""
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
# Obtener última versión # Obtener última versión
version_meta, file_path = get_latest_version(project_id, document_id) version_meta, file_path = get_latest_version(project_id, document_id)
if not version_meta or not file_path: if not version_meta or not file_path:
flash('Documento no encontrado.', 'danger') flash("Documento no encontrado.", "danger")
return redirect(url_for('projects.view', project_id=project_id)) return redirect(url_for("projects.view", project_id=project_id))
# Registrar descarga # 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 # Enviar archivo
try: try:
return send_file( return send_file(
file_path, file_path,
mimetype=version_meta['mime_type'], mimetype=version_meta["mime_type"],
as_attachment=True, as_attachment=True,
download_name=os.path.basename(file_path) download_name=os.path.basename(file_path),
) )
except Exception as e: except Exception as e:
flash(f'Error al descargar el archivo: {str(e)}', 'danger') flash(f"Error al descargar el archivo: {str(e)}", "danger")
return redirect(url_for('documents.versions', project_id=project_id, document_id=document_id)) return redirect(
url_for(
"documents.versions", project_id=project_id, document_id=document_id
)
)
@documents_bp.route('/<int:project_id>/<int:document_id>/delete', methods=['POST'])
@documents_bp.route("/<project_id>/<int:document_id>/delete", methods=["POST"])
@login_required @login_required
@permission_required(9000) # Nivel alto para eliminar documentos @permission_required(9000) # Nivel alto para eliminar documentos
def delete(project_id, document_id): def delete(project_id, document_id):
@ -234,35 +270,37 @@ def delete(project_id, document_id):
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
success, message = delete_document(project_id, document_id) success, message = delete_document(project_id, document_id)
if success: if success:
flash(message, 'success') flash(message, "success")
else: else:
flash(message, 'danger') flash(message, "danger")
return redirect(url_for('projects.view', project_id=project_id)) return redirect(url_for("projects.view", project_id=project_id))
@documents_bp.route('/<int:project_id>/export')
@documents_bp.route("/<project_id>/export")
@login_required @login_required
def export(project_id): def export(project_id):
"""Exportar documentos de un proyecto.""" """Exportar documentos de un proyecto."""
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
documents = get_project_documents(project_id) documents = get_project_documents(project_id)
return render_template('documents/export.html', return render_template(
project=project, "documents/export.html", project=project, documents=documents
documents=documents) )
@documents_bp.route('/<int:project_id>/api/list')
@documents_bp.route("/<project_id>/api/list")
@login_required @login_required
def api_list(project_id): def api_list(project_id):
"""API para listar documentos de un proyecto.""" """API para listar documentos de un proyecto."""

View File

@ -5,37 +5,53 @@ from wtforms import StringField, SelectField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length from wtforms.validators import DataRequired, Length
from services.project_service import ( from services.project_service import (
create_project, update_project, get_project, delete_project, create_project,
get_all_projects, get_project_children, get_project_document_count, update_project,
filter_projects 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 services.schema_service import get_all_schemas
from utils.security import permission_required from utils.security import permission_required
# Definir Blueprint # Definir Blueprint
projects_bp = Blueprint('projects', __name__, url_prefix='/projects') projects_bp = Blueprint("projects", __name__, url_prefix="/projects")
# Formularios # Formularios
class ProjectForm(FlaskForm): class ProjectForm(FlaskForm):
"""Formulario de proyecto.""" """Formulario de proyecto."""
descripcion = StringField('Descripción', validators=[DataRequired(), Length(1, 100)])
cliente = StringField('Cliente', validators=[DataRequired(), Length(1, 100)]) descripcion = StringField(
destinacion = StringField('Destinación', validators=[Length(0, 100)]) "Descripción", validators=[DataRequired(), Length(1, 100)]
esquema = SelectField('Esquema', validators=[DataRequired()]) )
proyecto_padre = SelectField('Proyecto Padre', validators=[]) cliente = StringField("Cliente", validators=[DataRequired(), Length(1, 100)])
submit = SubmitField('Guardar') 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): class ProjectFilterForm(FlaskForm):
"""Formulario de filtrado de proyectos.""" """Formulario de filtrado de proyectos."""
cliente = StringField('Cliente')
estado = SelectField('Estado', choices=[('', 'Todos'), ('activo', 'Activo'), ('inactivo', 'Inactivo')]) cliente = StringField("Cliente")
ano_inicio = StringField('Año Inicio') estado = SelectField(
ano_fin = StringField('Año Fin') "Estado",
descripcion = StringField('Descripción') choices=[("", "Todos"), ("activo", "Activo"), ("inactivo", "Inactivo")],
submit = SubmitField('Filtrar') )
ano_inicio = StringField("Año Inicio")
ano_fin = StringField("Año Fin")
descripcion = StringField("Descripción")
submit = SubmitField("Filtrar")
# Rutas # Rutas
@projects_bp.route('/') @projects_bp.route("/")
@login_required @login_required
def list(): def list():
"""Listar proyectos.""" """Listar proyectos."""
@ -47,11 +63,12 @@ def list():
else: else:
projects = get_all_projects() projects = get_all_projects()
return render_template('projects/list.html', return render_template(
projects=projects, "projects/list.html", projects=projects, filter_form=filter_form
filter_form=filter_form) )
@projects_bp.route('/create', methods=['GET', 'POST'])
@projects_bp.route("/create", methods=["GET", "POST"])
@login_required @login_required
@permission_required(1000) # Nivel mínimo para crear proyectos @permission_required(1000) # Nivel mínimo para crear proyectos
def create(): def create():
@ -60,53 +77,64 @@ def create():
# Cargar opciones para esquemas # Cargar opciones para esquemas
schemas = get_all_schemas() 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 # Cargar opciones para proyectos padre
projects = [(p['codigo'], p['descripcion']) for p in get_all_projects()] projects = [(p["codigo"], p["descripcion"]) for p in get_all_projects()]
form.proyecto_padre.choices = [('', 'Ninguno')] + projects form.proyecto_padre.choices = [("", "Ninguno")] + projects
if form.validate_on_submit(): if form.validate_on_submit():
# Preparar datos del proyecto # Preparar datos del proyecto
project_data = { project_data = {
'descripcion': form.descripcion.data, "descripcion": form.descripcion.data,
'cliente': form.cliente.data, "cliente": form.cliente.data,
'destinacion': form.destinacion.data, "destinacion": form.destinacion.data,
'esquema': form.esquema.data, "esquema": form.esquema.data,
'proyecto_padre': form.proyecto_padre.data if form.proyecto_padre.data else None "proyecto_padre": (
form.proyecto_padre.data if form.proyecto_padre.data else None
),
} }
# Crear proyecto # Crear proyecto
success, message, project_id = create_project(project_data, current_user.username) success, message, project_id = create_project(
project_data, current_user.username
)
if success: if success:
flash(message, 'success') flash(message, "success")
return redirect(url_for('projects.view', project_id=project_id)) return redirect(url_for("projects.view", project_id=project_id))
else: else:
flash(message, 'danger') flash(message, "danger")
return render_template('projects/create.html', form=form) return render_template("projects/create.html", form=form, schemas=schemas)
@projects_bp.route('/<int:project_id>')
@projects_bp.route("/<project_id>")
@login_required @login_required
def view(project_id): def view(project_id):
"""Ver detalles de un proyecto.""" """Ver detalles de un proyecto."""
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
# Obtener información adicional # Obtener información adicional
children = get_project_children(project_id) children = get_project_children(project_id)
document_count = get_project_document_count(project_id) document_count = get_project_document_count(project_id)
return render_template('projects/view.html', return render_template(
"projects/view.html",
project=project, project=project,
children=children, children=children,
document_count=document_count) document_count=document_count,
)
@projects_bp.route('/<int:project_id>/edit', methods=['GET', 'POST'])
@projects_bp.route("/<project_id>/edit", methods=["GET", "POST"])
@login_required @login_required
@permission_required(5000) # Nivel mínimo para editar proyectos @permission_required(5000) # Nivel mínimo para editar proyectos
def edit(project_id): def edit(project_id):
@ -114,53 +142,66 @@ def edit(project_id):
project = get_project(project_id) project = get_project(project_id)
if not project: if not project:
flash('Proyecto no encontrado.', 'danger') flash("Proyecto no encontrado.", "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
form = ProjectForm() form = ProjectForm()
# Cargar opciones para esquemas # Cargar opciones para esquemas
schemas = get_all_schemas() 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) # Cargar opciones para proyectos padre (excluyendo este proyecto y sus hijos)
all_projects = get_all_projects() all_projects = get_all_projects()
children_codes = [child['codigo'] for child in get_project_children(project_id)] children_codes = [child["codigo"] for child in get_project_children(project_id)]
available_projects = [(p['codigo'], p['descripcion']) for p in all_projects available_projects = [
if p['codigo'] != project['codigo'] and p['codigo'] not in children_codes] (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 form.proyecto_padre.choices = [("", "Ninguno")] + available_projects
if request.method == 'GET': if request.method == "GET":
# Cargar datos actuales # Cargar datos actuales
form.descripcion.data = project['descripcion'] form.descripcion.data = project["descripcion"]
form.cliente.data = project['cliente'] form.cliente.data = project["cliente"]
form.destinacion.data = project.get('destinacion', '') form.destinacion.data = project.get("destinacion", "")
form.esquema.data = project['esquema'] form.esquema.data = project["esquema"]
form.proyecto_padre.data = project.get('proyecto_padre', '') form.proyecto_padre.data = project.get("proyecto_padre", "")
if form.validate_on_submit(): if form.validate_on_submit():
# Preparar datos actualizados # Preparar datos actualizados
project_data = { project_data = {
'descripcion': form.descripcion.data, "descripcion": form.descripcion.data,
'cliente': form.cliente.data, "cliente": form.cliente.data,
'destinacion': form.destinacion.data, "destinacion": form.destinacion.data,
'esquema': form.esquema.data, "esquema": form.esquema.data,
'proyecto_padre': form.proyecto_padre.data if form.proyecto_padre.data else None "proyecto_padre": (
form.proyecto_padre.data if form.proyecto_padre.data else None
),
} }
# Actualizar proyecto # Actualizar proyecto
success, message = update_project(project_id, project_data, current_user.username) success, message = update_project(
project_id, project_data, current_user.username
)
if success: if success:
flash(message, 'success') flash(message, "success")
return redirect(url_for('projects.view', project_id=project_id)) return redirect(url_for("projects.view", project_id=project_id))
else: else:
flash(message, 'danger') flash(message, "danger")
return render_template('projects/edit.html', form=form, project=project) return render_template(
"projects/edit.html", form=form, project=project, schemas=schemas
)
@projects_bp.route('/<int:project_id>/delete', methods=['POST'])
@projects_bp.route("/<project_id>/delete", methods=["POST"])
@login_required @login_required
@permission_required(9000) # Nivel alto para eliminar proyectos @permission_required(9000) # Nivel alto para eliminar proyectos
def delete(project_id): def delete(project_id):
@ -168,18 +209,31 @@ def delete(project_id):
success, message = delete_project(project_id) success, message = delete_project(project_id)
if success: if success:
flash(message, 'success') flash(message, "success")
else: else:
flash(message, 'danger') flash(message, "danger")
return redirect(url_for('projects.list')) return redirect(url_for("projects.list"))
@projects_bp.route('/api/list')
@projects_bp.route("/<project_id>/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 @login_required
def api_list(): def api_list():
"""API para listar proyectos (para selects dinámicos).""" """API para listar proyectos (para selects dinámicos)."""
projects = get_all_projects() projects = get_all_projects()
return jsonify([{ return jsonify([{"id": p["codigo"], "text": p["descripcion"]} for p in projects])
'id': p['codigo'],
'text': p['descripcion']
} for p in projects])

View File

@ -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_login import login_required, current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import ( from wtforms import (
@ -106,43 +115,39 @@ def create():
@schemas_bp.route("/edit/<schema_id>", methods=["GET", "POST"]) @schemas_bp.route("/edit/<schema_id>", methods=["GET", "POST"])
@login_required @login_required
@permission_required(9000) # Solo administradores def edit_schema(schema_id):
def edit(schema_id):
"""Editar esquema existente."""
schema = get_schema_by_id(schema_id) schema = get_schema_by_id(schema_id)
if not schema: if not schema:
flash("Esquema no encontrado.", "danger") flash("Esquema no encontrado", "danger")
return redirect(url_for("schemas.list")) return redirect(url_for("schemas.list"))
# Prepopulate form filetypes = get_allowed_filetypes()
form = SchemaForm(obj=schema)
if form.validate_on_submit(): if request.method == "POST":
data = { # Extract form data
"codigo": form.codigo.data, nombre = request.form.get("nombre")
"descripcion": form.descripcion.data, descripcion = request.form.get("descripcion")
"documentos": [],
# 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: success = update_schema(schema_id, updated_schema)
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)
if success: if success:
flash(message, "success") flash("Esquema actualizado correctamente", "success")
return redirect(url_for("schemas.list")) return redirect(url_for("schemas.list"))
else: 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/<schema_id>", methods=["POST"]) @schemas_bp.route("/delete/<schema_id>", methods=["POST"])
@ -177,7 +182,7 @@ def api_list():
@login_required @login_required
def api_get(schema_code): def api_get(schema_code):
"""API para obtener un esquema específico.""" """API para obtener un esquema específico."""
schema = get_schema(schema_code) schema = get_schema_by_id(schema_code)
if not schema: if not schema:
return jsonify({"error": "Esquema no encontrado"}), 404 return jsonify({"error": "Esquema no encontrado"}), 404

View File

@ -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))

View File

@ -7,24 +7,27 @@ from werkzeug.utils import secure_filename
from flask import current_app from flask import current_app
from utils.file_utils import ( from utils.file_utils import (
load_json_file, save_json_file, ensure_dir_exists, load_json_file,
get_next_id, format_document_directory_name, save_json_file,
format_version_filename ensure_dir_exists,
get_next_id,
format_document_directory_name,
format_version_filename,
) )
from utils.security import calculate_checksum, check_file_type from utils.security import calculate_checksum, check_file_type
from services.project_service import find_project_directory from services.project_service import find_project_directory
def get_allowed_filetypes(): def get_allowed_filetypes():
""" """Get all allowed filetypes from storage."""
Obtener los tipos de archivo permitidos. storage_path = current_app.config["STORAGE_PATH"]
filetypes_path = os.path.join(storage_path, "filetypes", "filetypes.json")
Returns: if os.path.exists(filetypes_path):
dict: Diccionario de tipos de archivo with open(filetypes_path, "r", encoding="utf-8") as f:
""" return json.load(f)
storage_path = current_app.config['STORAGE_PATH'] return {}
filetypes_file = os.path.join(storage_path, 'filetypes', 'filetypes.json')
return load_json_file(filetypes_file, {})
def add_document(project_id, document_data, file, creator_username): def add_document(project_id, document_data, file, creator_username):
""" """
@ -46,27 +49,27 @@ def add_document(project_id, document_data, file, creator_username):
return False, f"No se encontró el proyecto con ID {project_id}.", None return False, f"No se encontró el proyecto con ID {project_id}.", None
# Validar datos obligatorios # 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 return False, "El nombre del documento es obligatorio.", None
# Validar tipo de archivo # Validar tipo de archivo
allowed_filetypes = get_allowed_filetypes() allowed_filetypes = get_allowed_filetypes()
filename = secure_filename(file.filename) 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: if extension not in allowed_filetypes:
return False, f"Tipo de archivo no permitido: {extension}", None return False, f"Tipo de archivo no permitido: {extension}", None
# Verificar MIME type # 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 return False, "El tipo de archivo no coincide con su extensión.", None
# Obtener siguiente ID de documento # Obtener siguiente ID de documento
document_id = get_next_id('document') document_id = get_next_id("document")
# Preparar directorio del documento # Preparar directorio del documento
documents_dir = os.path.join(project_dir, 'documents') documents_dir = os.path.join(project_dir, "documents")
dir_name = format_document_directory_name(document_id, document_data['nombre']) dir_name = format_document_directory_name(document_id, document_data["nombre"])
document_dir = os.path.join(documents_dir, dir_name) document_dir = os.path.join(documents_dir, dir_name)
# Verificar si ya existe # Verificar si ya existe
@ -78,7 +81,9 @@ def add_document(project_id, document_data, file, creator_username):
# Preparar primera versión # Preparar primera versión
version = 1 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) version_path = os.path.join(document_dir, version_filename)
# Guardar archivo # Guardar archivo
@ -93,29 +98,30 @@ def add_document(project_id, document_data, file, creator_username):
# Preparar metadatos del documento # Preparar metadatos del documento
document_meta = { document_meta = {
'document_id': f"{document_id:03d}_{document_data['nombre'].lower().replace(' ', '_')}", "document_id": f"{document_id:03d}_{document_data['nombre'].lower().replace(' ', '_')}",
'original_filename': filename, "original_filename": filename,
'versions': [ "versions": [
{ {
'version': version, "version": version,
'filename': version_filename, "filename": version_filename,
'created_at': datetime.now(pytz.UTC).isoformat(), "created_at": datetime.now(pytz.UTC).isoformat(),
'created_by': creator_username, "created_by": creator_username,
'description': document_data.get('description', 'Versión inicial'), "description": document_data.get("description", "Versión inicial"),
'file_size': file_size, "file_size": file_size,
'mime_type': allowed_filetypes[extension]['mime_type'], "mime_type": allowed_filetypes[extension]["mime_type"],
'checksum': checksum, "checksum": checksum,
'downloads': [] "downloads": [],
} }
] ],
} }
# Guardar metadatos # 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) save_json_file(meta_file, document_meta)
return True, "Documento añadido correctamente.", document_id return True, "Documento añadido correctamente.", document_id
def add_version(project_id, document_id, version_data, file, creator_username): def add_version(project_id, document_id, version_data, file, creator_username):
""" """
Añadir una nueva versión a un documento existente. Añadir una nueva versión a un documento existente.
@ -143,7 +149,7 @@ def add_version(project_id, document_id, version_data, file, creator_username):
return False, f"No se encontró el documento con ID {document_id}.", None return False, f"No se encontró el documento con ID {document_id}.", None
# Cargar metadatos del documento # 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) document_meta = load_json_file(meta_file)
if not document_meta: if not document_meta:
@ -152,21 +158,25 @@ def add_version(project_id, document_id, version_data, file, creator_username):
# Validar tipo de archivo # Validar tipo de archivo
allowed_filetypes = get_allowed_filetypes() allowed_filetypes = get_allowed_filetypes()
filename = secure_filename(file.filename) 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: if extension not in allowed_filetypes:
return False, f"Tipo de archivo no permitido: {extension}", None return False, f"Tipo de archivo no permitido: {extension}", None
# Verificar MIME type # 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 return False, "El tipo de archivo no coincide con su extensión.", None
# Determinar número de versión # 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 new_version = last_version + 1
# Preparar nombre de archivo # 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_filename = format_version_filename(new_version, doc_name, extension)
version_path = os.path.join(document_dir, version_filename) version_path = os.path.join(document_dir, version_filename)
@ -182,25 +192,26 @@ def add_version(project_id, document_id, version_data, file, creator_username):
# Preparar metadatos de la versión # Preparar metadatos de la versión
version_meta = { version_meta = {
'version': new_version, "version": new_version,
'filename': version_filename, "filename": version_filename,
'created_at': datetime.now(pytz.UTC).isoformat(), "created_at": datetime.now(pytz.UTC).isoformat(),
'created_by': creator_username, "created_by": creator_username,
'description': version_data.get('description', f'Versión {new_version}'), "description": version_data.get("description", f"Versión {new_version}"),
'file_size': file_size, "file_size": file_size,
'mime_type': allowed_filetypes[extension]['mime_type'], "mime_type": allowed_filetypes[extension]["mime_type"],
'checksum': checksum, "checksum": checksum,
'downloads': [] "downloads": [],
} }
# Añadir versión a metadatos # Añadir versión a metadatos
document_meta['versions'].append(version_meta) document_meta["versions"].append(version_meta)
# Guardar metadatos actualizados # Guardar metadatos actualizados
save_json_file(meta_file, document_meta) save_json_file(meta_file, document_meta)
return True, "Nueva versión añadida correctamente.", new_version return True, "Nueva versión añadida correctamente.", new_version
def get_document(project_id, document_id): def get_document(project_id, document_id):
""" """
Obtener información de un documento. Obtener información de un documento.
@ -225,15 +236,16 @@ def get_document(project_id, document_id):
return None return None
# Cargar metadatos # 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) document_meta = load_json_file(meta_file)
# Agregar ruta del directorio # Agregar ruta del directorio
if document_meta: if document_meta:
document_meta['directory'] = os.path.basename(document_dir) document_meta["directory"] = os.path.basename(document_dir)
return document_meta return document_meta
def get_document_version(project_id, document_id, version): def get_document_version(project_id, document_id, version):
""" """
Obtener información de una versión específica de un documento. Obtener información de una versión específica de un documento.
@ -253,8 +265,8 @@ def get_document_version(project_id, document_id, version):
# Buscar versión específica # Buscar versión específica
version_meta = None version_meta = None
for v in document['versions']: for v in document["versions"]:
if v['version'] == int(version): if v["version"] == int(version):
version_meta = v version_meta = v
break break
@ -264,10 +276,11 @@ def get_document_version(project_id, document_id, version):
# Preparar ruta al archivo # Preparar ruta al archivo
project_dir = find_project_directory(project_id) project_dir = find_project_directory(project_id)
document_dir = find_document_directory(project_dir, document_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 return version_meta, file_path
def get_latest_version(project_id, document_id): def get_latest_version(project_id, document_id):
""" """
Obtener la última versión de un documento. Obtener la última versión de un documento.
@ -281,19 +294,20 @@ def get_latest_version(project_id, document_id):
""" """
document = get_document(project_id, document_id) document = get_document(project_id, document_id)
if not document or not document['versions']: if not document or not document["versions"]:
return None, None return None, None
# Encontrar la versión más reciente # 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 # Preparar ruta al archivo
project_dir = find_project_directory(project_id) project_dir = find_project_directory(project_id)
document_dir = find_document_directory(project_dir, document_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 return latest_version, file_path
def register_download(project_id, document_id, version, username): def register_download(project_id, document_id, version, username):
""" """
Registrar una descarga de documento. Registrar una descarga de documento.
@ -319,21 +333,21 @@ def register_download(project_id, document_id, version, username):
return False return False
# Cargar metadatos # 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) document_meta = load_json_file(meta_file)
if not document_meta: if not document_meta:
return False return False
# Buscar versión # Buscar versión
for v in document_meta['versions']: for v in document_meta["versions"]:
if v['version'] == int(version): if v["version"] == int(version):
# Registrar descarga # Registrar descarga
download_info = { download_info = {
'user_id': username, "user_id": username,
'downloaded_at': datetime.now(pytz.UTC).isoformat() "downloaded_at": datetime.now(pytz.UTC).isoformat(),
} }
v['downloads'].append(download_info) v["downloads"].append(download_info)
# Guardar metadatos actualizados # Guardar metadatos actualizados
save_json_file(meta_file, document_meta) save_json_file(meta_file, document_meta)
@ -341,6 +355,7 @@ def register_download(project_id, document_id, version, username):
return False return False
def find_document_directory(project_dir, document_id): def find_document_directory(project_dir, document_id):
""" """
Encontrar el directorio de un documento por su ID. Encontrar el directorio de un documento por su ID.
@ -352,7 +367,7 @@ def find_document_directory(project_dir, document_id):
Returns: Returns:
str: Ruta al directorio o None si no se encuentra 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): if not os.path.exists(documents_dir):
return None return None
@ -366,6 +381,7 @@ def find_document_directory(project_dir, document_id):
return None return None
def get_project_documents(project_id): def get_project_documents(project_id):
""" """
Obtener todos los documentos de un proyecto. Obtener todos los documentos de un proyecto.
@ -381,7 +397,7 @@ def get_project_documents(project_id):
if not project_dir: if not project_dir:
return [] return []
documents_dir = os.path.join(project_dir, 'documents') documents_dir = os.path.join(project_dir, "documents")
if not os.path.exists(documents_dir): if not os.path.exists(documents_dir):
return [] return []
@ -390,9 +406,11 @@ def get_project_documents(project_id):
# Iterar sobre directorios de documentos # Iterar sobre directorios de documentos
for dir_name in os.listdir(documents_dir): 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) 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): if os.path.exists(meta_file):
document_meta = load_json_file(meta_file) document_meta = load_json_file(meta_file)
@ -400,15 +418,16 @@ def get_project_documents(project_id):
if document_meta: if document_meta:
# Extraer ID del documento del nombre del directorio # Extraer ID del documento del nombre del directorio
try: try:
doc_id = int(dir_name.split('_', 1)[0].replace('@', '')) doc_id = int(dir_name.split("_", 1)[0].replace("@", ""))
document_meta['id'] = doc_id document_meta["id"] = doc_id
document_meta['directory'] = dir_name document_meta["directory"] = dir_name
documents.append(document_meta) documents.append(document_meta)
except (ValueError, IndexError): except (ValueError, IndexError):
pass pass
return documents return documents
def delete_document(project_id, document_id): def delete_document(project_id, document_id):
""" """
Eliminar un documento. Eliminar un documento.
@ -434,6 +453,7 @@ def delete_document(project_id, document_id):
# Eliminar directorio y contenido # Eliminar directorio y contenido
try: try:
import shutil import shutil
shutil.rmtree(document_dir) shutil.rmtree(document_dir)
return True, "Documento eliminado correctamente." return True, "Documento eliminado correctamente."
except Exception as e: except Exception as e:

View File

@ -4,10 +4,14 @@ from datetime import datetime
import pytz import pytz
from flask import current_app from flask import current_app
from utils.file_utils import ( from utils.file_utils import (
load_json_file, save_json_file, ensure_dir_exists, load_json_file,
get_next_id, format_project_directory_name save_json_file,
ensure_dir_exists,
get_next_id,
format_project_directory_name,
) )
def create_project(project_data, creator_username): def create_project(project_data, creator_username):
""" """
Crear un nuevo proyecto. Crear un nuevo proyecto.
@ -20,21 +24,21 @@ def create_project(project_data, creator_username):
tuple: (success, message, project_id) tuple: (success, message, project_id)
""" """
# Validar datos obligatorios # Validar datos obligatorios
required_fields = ['descripcion', 'cliente', 'esquema'] required_fields = ["descripcion", "cliente", "esquema"]
for field in required_fields: for field in required_fields:
if field not in project_data or not project_data[field]: if field not in project_data or not project_data[field]:
return False, f"El campo '{field}' es obligatorio.", None return False, f"El campo '{field}' es obligatorio.", None
# Obtener siguiente ID de proyecto # Obtener siguiente ID de proyecto
project_id = get_next_id('project') project_id = get_next_id("project")
# Crear código de proyecto (PROJ001, etc.) # Crear código de proyecto (PROJ001, etc.)
project_code = f"PROJ{project_id:03d}" project_code = f"PROJ{project_id:03d}"
# Preparar directorio del proyecto # Preparar directorio del proyecto
storage_path = current_app.config['STORAGE_PATH'] storage_path = current_app.config["STORAGE_PATH"]
dir_name = format_project_directory_name(project_id, project_data['descripcion']) dir_name = format_project_directory_name(project_id, project_data["descripcion"])
project_dir = os.path.join(storage_path, 'projects', dir_name) project_dir = os.path.join(storage_path, "projects", dir_name)
# Verificar si ya existe # Verificar si ya existe
if os.path.exists(project_dir): if os.path.exists(project_dir):
@ -42,42 +46,43 @@ def create_project(project_data, creator_username):
# Crear directorios # Crear directorios
ensure_dir_exists(project_dir) 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 # Crear metadatos del proyecto
project_meta = { project_meta = {
'codigo': project_code, "codigo": project_code,
'proyecto_padre': project_data.get('proyecto_padre'), "proyecto_padre": project_data.get("proyecto_padre"),
'esquema': project_data['esquema'], "esquema": project_data["esquema"],
'descripcion': project_data['descripcion'], "descripcion": project_data["descripcion"],
'cliente': project_data['cliente'], "cliente": project_data["cliente"],
'destinacion': project_data.get('destinacion', ''), "destinacion": project_data.get("destinacion", ""),
'ano_creacion': datetime.now().year, "ano_creacion": datetime.now().year,
'fecha_creacion': datetime.now(pytz.UTC).isoformat(), "fecha_creacion": datetime.now(pytz.UTC).isoformat(),
'creado_por': creator_username, "creado_por": creator_username,
'estado': 'activo', "estado": "activo",
'ultima_modificacion': datetime.now(pytz.UTC).isoformat(), "ultima_modificacion": datetime.now(pytz.UTC).isoformat(),
'modificado_por': creator_username "modificado_por": creator_username,
} }
# Guardar metadatos # 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) save_json_file(meta_file, project_meta)
# Guardar permisos del proyecto (inicialmente vacío) # 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, {}) save_json_file(permissions_file, {})
# Copiar el esquema seleccionado # 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, {}) schemas = load_json_file(schema_file, {})
if project_data['esquema'] in schemas: if project_data["esquema"] in schemas:
project_schema_file = os.path.join(project_dir, 'schema.json') project_schema_file = os.path.join(project_dir, "schema.json")
save_json_file(project_schema_file, schemas[project_data['esquema']]) save_json_file(project_schema_file, schemas[project_data["esquema"]])
return True, "Proyecto creado correctamente.", project_id return True, "Proyecto creado correctamente.", project_id
def update_project(project_id, project_data, modifier_username): def update_project(project_id, project_data, modifier_username):
""" """
Actualizar un proyecto existente. Actualizar un proyecto existente.
@ -97,23 +102,29 @@ def update_project(project_id, project_data, modifier_username):
return False, f"No se encontró el proyecto con ID {project_id}." return False, f"No se encontró el proyecto con ID {project_id}."
# Cargar metadatos actuales # 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) current_meta = load_json_file(meta_file)
# Actualizar campos # Actualizar campos
for key, value in project_data.items(): 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 current_meta[key] = value
# Actualizar metadatos de modificación # Actualizar metadatos de modificación
current_meta['ultima_modificacion'] = datetime.now(pytz.UTC).isoformat() current_meta["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat()
current_meta['modificado_por'] = modifier_username current_meta["modificado_por"] = modifier_username
# Guardar metadatos actualizados # Guardar metadatos actualizados
save_json_file(meta_file, current_meta) save_json_file(meta_file, current_meta)
return True, "Proyecto actualizado correctamente." return True, "Proyecto actualizado correctamente."
def get_project(project_id): def get_project(project_id):
""" """
Obtener información de un proyecto. Obtener información de un proyecto.
@ -130,14 +141,15 @@ def get_project(project_id):
return None return None
# Cargar metadatos # 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) project_meta = load_json_file(meta_file)
# Agregar la ruta del directorio # Agregar la ruta del directorio
project_meta['directory'] = os.path.basename(project_dir) project_meta["directory"] = os.path.basename(project_dir)
return project_meta return project_meta
def delete_project(project_id): def delete_project(project_id):
""" """
Eliminar un proyecto (marcar como inactivo). Eliminar un proyecto (marcar como inactivo).
@ -154,18 +166,19 @@ def delete_project(project_id):
return False, f"No se encontró el proyecto con ID {project_id}." return False, f"No se encontró el proyecto con ID {project_id}."
# Cargar metadatos # 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) project_meta = load_json_file(meta_file)
# Marcar como inactivo (no eliminar físicamente) # Marcar como inactivo (no eliminar físicamente)
project_meta['estado'] = 'inactivo' project_meta["estado"] = "inactivo"
project_meta['ultima_modificacion'] = datetime.now(pytz.UTC).isoformat() project_meta["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat()
# Guardar metadatos actualizados # Guardar metadatos actualizados
save_json_file(meta_file, project_meta) save_json_file(meta_file, project_meta)
return True, "Proyecto marcado como inactivo." return True, "Proyecto marcado como inactivo."
def get_all_projects(include_inactive=False): def get_all_projects(include_inactive=False):
""" """
Obtener todos los proyectos. Obtener todos los proyectos.
@ -176,29 +189,30 @@ def get_all_projects(include_inactive=False):
Returns: Returns:
list: Lista de proyectos list: Lista de proyectos
""" """
storage_path = current_app.config['STORAGE_PATH'] storage_path = current_app.config["STORAGE_PATH"]
projects_dir = os.path.join(storage_path, 'projects') projects_dir = os.path.join(storage_path, "projects")
projects = [] projects = []
# Iterar sobre directorios de proyectos # Iterar sobre directorios de proyectos
for dir_name in os.listdir(projects_dir): 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) project_dir = os.path.join(projects_dir, dir_name)
if os.path.isdir(project_dir): 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): if os.path.exists(meta_file):
project_meta = load_json_file(meta_file) project_meta = load_json_file(meta_file)
# Incluir solo si está activo o se solicitan inactivos # 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 # Agregar la ruta del directorio
project_meta['directory'] = dir_name project_meta["directory"] = dir_name
projects.append(project_meta) projects.append(project_meta)
return projects return projects
def find_project_directory(project_id): def find_project_directory(project_id):
""" """
Encontrar el directorio de un proyecto por su ID. Encontrar el directorio de un proyecto por su ID.
@ -209,8 +223,8 @@ def find_project_directory(project_id):
Returns: Returns:
str: Ruta al directorio o None si no se encuentra str: Ruta al directorio o None si no se encuentra
""" """
storage_path = current_app.config['STORAGE_PATH'] storage_path = current_app.config["STORAGE_PATH"]
projects_dir = os.path.join(storage_path, 'projects') projects_dir = os.path.join(storage_path, "projects")
# Prefijo a buscar en nombres de directorios # Prefijo a buscar en nombres de directorios
prefix = f"@{int(project_id):03d}_@" prefix = f"@{int(project_id):03d}_@"
@ -221,6 +235,7 @@ def find_project_directory(project_id):
return None return None
def get_project_children(project_id): def get_project_children(project_id):
""" """
Obtener proyectos hijos de un proyecto. Obtener proyectos hijos de un proyecto.
@ -235,10 +250,11 @@ def get_project_children(project_id):
project_code = f"PROJ{int(project_id):03d}" project_code = f"PROJ{int(project_id):03d}"
# Filtrar proyectos con este padre # 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 return children
def get_project_document_count(project_id): def get_project_document_count(project_id):
""" """
Contar documentos en un proyecto. Contar documentos en un proyecto.
@ -254,7 +270,7 @@ def get_project_document_count(project_id):
if not project_dir: if not project_dir:
return 0 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): if not os.path.exists(documents_dir):
return 0 return 0
@ -262,11 +278,12 @@ def get_project_document_count(project_id):
# Contar directorios de documentos # Contar directorios de documentos
count = 0 count = 0
for item in os.listdir(documents_dir): 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 count += 1
return count return count
def filter_projects(filter_params): def filter_projects(filter_params):
""" """
Filtrar proyectos según los parámetros proporcionados. Filtrar proyectos según los parámetros proporcionados.
@ -283,36 +300,70 @@ def filter_projects(filter_params):
list: Lista de proyectos que cumplen los criterios list: Lista de proyectos que cumplen los criterios
""" """
# Obtener todos los proyectos (incluyendo inactivos si se solicitan) # 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) all_projects = get_all_projects(include_inactive)
filtered_projects = [] filtered_projects = []
for project in all_projects: for project in all_projects:
# Filtrar por cliente # Filtrar por cliente
if 'cliente' in filter_params and filter_params['cliente']: if "cliente" in filter_params and filter_params["cliente"]:
if project['cliente'] != filter_params['cliente']: if project["cliente"] != filter_params["cliente"]:
continue continue
# Filtrar por estado # Filtrar por estado
if 'estado' in filter_params and filter_params['estado']: if "estado" in filter_params and filter_params["estado"]:
if project['estado'] != filter_params['estado']: if project["estado"] != filter_params["estado"]:
continue continue
# Filtrar por año de creación (rango) # Filtrar por año de creación (rango)
if 'ano_inicio' in filter_params and filter_params['ano_inicio']: if "ano_inicio" in filter_params and filter_params["ano_inicio"]:
if project['ano_creacion'] < int(filter_params['ano_inicio']): if project["ano_creacion"] < int(filter_params["ano_inicio"]):
continue continue
if 'ano_fin' in filter_params and filter_params['ano_fin']: if "ano_fin" in filter_params and filter_params["ano_fin"]:
if project['ano_creacion'] > int(filter_params['ano_fin']): if project["ano_creacion"] > int(filter_params["ano_fin"]):
continue continue
# Filtrar por término en descripción # Filtrar por término en descripción
if 'descripcion' in filter_params and filter_params['descripcion']: if "descripcion" in filter_params and filter_params["descripcion"]:
if filter_params['descripcion'].lower() not in project['descripcion'].lower(): if (
filter_params["descripcion"].lower()
not in project["descripcion"].lower()
):
continue continue
# Si pasó todos los filtros, agregar a la lista # Si pasó todos los filtros, agregar a la lista
filtered_projects.append(project) filtered_projects.append(project)
return filtered_projects 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."

View File

@ -6,29 +6,44 @@ from flask import current_app
from utils.file_utils import load_json_file, save_json_file from utils.file_utils import load_json_file, save_json_file
def get_schemas_file_path(): def get_schema_file_path():
"""Obtener ruta al archivo de esquemas.""" """Get path to the schema storage file."""
storage_path = current_app.config["STORAGE_PATH"] storage_path = current_app.config["STORAGE_PATH"]
return os.path.join(storage_path, "schemas", "schema.json") 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(): def get_all_schemas():
"""Obtener todos los esquemas disponibles.""" """Get all schemas as a list."""
return load_json_file(get_schemas_file_path(), {}) 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): def get_schema_by_id(schema_id):
""" """Get a specific schema by ID."""
Obtener un esquema por su ID. schemas = load_schemas()
if schema_id in schemas:
Args: return {**schemas[schema_id], "id": schema_id}
schema_id (str): ID del esquema a buscar return None
Returns:
dict: Datos del esquema o None si no existe
"""
schemas = get_all_schemas()
return schemas.get(schema_id)
def create_schema(schema_data, user_id): def create_schema(schema_data, user_id):
@ -42,7 +57,7 @@ def create_schema(schema_data, user_id):
Returns: Returns:
tuple: (éxito, mensaje) tuple: (éxito, mensaje)
""" """
schemas = get_all_schemas() schemas = load_schemas()
# Verificar si ya existe un esquema con ese código # Verificar si ya existe un esquema con ese código
schema_id = schema_data["codigo"] schema_id = schema_data["codigo"]
@ -55,38 +70,25 @@ def create_schema(schema_data, user_id):
# Guardar esquema # Guardar esquema
schemas[schema_id] = schema_data 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." return True, f"Esquema '{schema_data['descripcion']}' creado correctamente."
def update_schema(schema_id, schema_data): def update_schema(schema_id, schema_data):
""" """Update an existing schema."""
Actualizar un esquema existente. schemas = load_schemas()
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
if schema_id not in 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 # Update modification timestamp
schema_data["fecha_creacion"] = schemas[schema_id].get("fecha_creacion") schema_data["updated_at"] = datetime.now().isoformat()
schema_data["creado_por"] = schemas[schema_id].get("creado_por")
schema_data["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat()
# Actualizar esquema # Remove id from data before saving (it's used as the key)
schemas[schema_id] = schema_data data_to_save = {k: v for k, v in schema_data.items() if k != "id"}
save_json_file(get_schemas_file_path(), schemas)
return True, f"Esquema '{schema_data['descripcion']}' actualizado correctamente." schemas[schema_id] = data_to_save
return save_schemas(schemas)
def delete_schema(schema_id): def delete_schema(schema_id):
@ -99,7 +101,7 @@ def delete_schema(schema_id):
Returns: Returns:
tuple: (éxito, mensaje) tuple: (éxito, mensaje)
""" """
schemas = get_all_schemas() schemas = load_schemas()
# Verificar si existe el esquema # Verificar si existe el esquema
if schema_id not in schemas: if schema_id not in schemas:
@ -111,51 +113,42 @@ def delete_schema(schema_id):
# Eliminar esquema # Eliminar esquema
schema_desc = schemas[schema_id].get("descripcion", schema_id) schema_desc = schemas[schema_id].get("descripcion", schema_id)
del schemas[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." return True, f"Esquema '{schema_desc}' eliminado correctamente."
def initialize_default_schemas(): def initialize_default_schemas():
"""Inicializar esquemas predeterminados si no existen.""" """Initialize default schemas if none exist."""
schemas = get_all_schemas() schemas = load_schemas()
if not schemas:
# Si ya hay esquemas, no hacer nada default_schemas = {
if schemas: "default": {
return "name": "Documento Estándar",
"description": "Esquema básico para documentos",
# Esquema predeterminado para proyecto estándar "fields": [
default_schema = { {"name": "title", "type": "text", "required": True},
"ESQ001": { {"name": "content", "type": "textarea", "required": True},
"codigo": "ESQ001", {"name": "tags", "type": "tags", "required": False},
"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,
},
], ],
"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_schemas(default_schemas)
save_json_file(get_schemas_file_path(), default_schema)
current_app.logger.info("Esquemas predeterminados inicializados.")
def get_schema_document_types(schema_id): def get_schema_document_types(schema_id):

View File

@ -1,45 +1,24 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Crear Proyecto - ARCH{% endblock %} {% block title %}Crear Nuevo Proyecto{% endblock %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-4">
<h1>Crear Nuevo Proyecto</h1> <h2>Crear Nuevo Proyecto</h2>
<div class="card"> <form method="POST">
<div class="card-header bg-primary text-white"> {{ form.csrf_token }}
<h5 class="mb-0">Información del Proyecto</h5> <!-- or use this alternative if not using WTForms: -->
</div> <!-- <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> -->
<div class="card-body">
<form method="POST" action="{{ url_for('projects.create') }}">
{{ form.csrf_token if form.csrf_token }}
<div class="mb-3">
<label for="codigo" class="form-label">Código del Proyecto</label>
<input type="text" class="form-control" id="codigo" name="codigo" required>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="descripcion" class="form-label">Descripción</label> <label for="descripcion" class="form-label">Descripción</label>
<textarea class="form-control" id="descripcion" name="descripcion" rows="3" required></textarea> <input type="text" class="form-control" id="descripcion" name="descripcion" required>
</div>
<div class="mb-3">
<label for="esquema" class="form-label">Esquema</label>
<select class="form-select" id="esquema" name="esquema" required>
{% if schemas %}
{% for codigo, schema in schemas.items() %}
<option value="{{ codigo }}">{{ codigo }} - {{ schema.descripcion }}</option>
{% endfor %}
{% else %}
<option value="">No hay esquemas disponibles</option>
{% endif %}
</select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="cliente" class="form-label">Cliente</label> <label for="cliente" class="form-label">Cliente</label>
<input type="text" class="form-control" id="cliente" name="cliente"> <input type="text" class="form-control" id="cliente" name="cliente" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -48,32 +27,33 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="proyecto_padre" class="form-label">Proyecto Padre (opcional)</label> <label for="esquema" class="form-label">Esquema</label>
<select class="form-select" id="proyecto_padre" name="proyecto_padre"> <select class="form-select" id="esquema" name="esquema" required>
<option value="">Ninguno</option> <option value="">Seleccione un esquema</option>
{% if projects %} {% if form.esquema.choices %}
{% for project in projects %} {% for value, label in form.esquema.choices %}
<option value="{{ project.id }}">{{ project.codigo }} - {{ project.descripcion }}</option> <option value="{{ value }}">{{ label }}</option>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="estado" class="form-label">Estado</label> <label for="proyecto_padre" class="form-label">Proyecto Padre</label>
<select class="form-select" id="estado" name="estado"> <select class="form-select" id="proyecto_padre" name="proyecto_padre">
<option value="activo" selected>Activo</option> <option value="">Ninguno</option>
<option value="archivado">Archivado</option> {% if form.proyecto_padre.choices %}
<option value="suspendido">Suspendido</option> {% for value, label in form.proyecto_padre.choices %}
{% if value %}
<option value="{{ value }}">{{ label }}</option>
{% endif %}
{% endfor %}
{% endif %}
</select> </select>
</div> </div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">Crear Proyecto</button> <button type="submit" class="btn btn-primary">Crear Proyecto</button>
<a href="{{ url_for('projects.list') }}" class="btn btn-secondary">Cancelar</a> <a href="{{ url_for('projects.list') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form> </form>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block title %}Editar Proyecto: {{ project.descripcion }}{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>Editar Proyecto</h2>
<form method="POST">
{{ form.csrf_token }}
<div class="mb-3">
<label for="descripcion" class="form-label">Descripción</label>
{{ form.descripcion(class="form-control", id="descripcion") }}
</div>
<div class="mb-3">
<label for="cliente" class="form-label">Cliente</label>
{{ form.cliente(class="form-control", id="cliente") }}
</div>
<div class="mb-3">
<label for="destinacion" class="form-label">Destinación</label>
{{ form.destinacion(class="form-control", id="destinacion") }}
</div>
<div class="mb-3">
<label for="esquema" class="form-label">Esquema</label>
{{ form.esquema(class="form-select", id="esquema") }}
</div>
<div class="mb-3">
<label for="proyecto_padre" class="form-label">Proyecto Padre</label>
{{ form.proyecto_padre(class="form-select", id="proyecto_padre") }}
</div>
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
<a href="{{ url_for('projects.view', project_id=project.codigo) }}" class="btn btn-secondary">Cancelar</a>
</form>
</div>
{% if project.estado == 'activo' %}
<div class="container mt-4">
<div class="card border-warning">
<div class="card-header bg-warning text-white">
<h5 class="mb-0">Acciones Administrativas</h5>
</div>
<div class="card-body">
<h5 class="card-title">Archivar Proyecto</h5>
<p class="card-text">Al archivar este proyecto, se marcará como inactivo y no se podrán añadir nuevos documentos.</p>
<form action="{{ url_for('projects.archive', project_id=project.codigo) }}" method="POST" class="d-inline">
{{ form.csrf_token }}
<button type="submit" class="btn btn-warning" onclick="return confirm('¿Está seguro de que desea archivar este proyecto?');">
<i class="fas fa-archive"></i> Archivar Proyecto
</button>
</form>
</div>
</div>
</div>
{% endif %}
<div class="container mt-4">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h5 class="mb-0">Zona de Peligro</h5>
</div>
<div class="card-body">
<h5 class="card-title">Eliminar Proyecto</h5>
<p class="card-text">Esta acción no se puede deshacer. Se eliminarán todos los datos asociados a este proyecto.</p>
<form action="{{ url_for('projects.delete', project_id=project.codigo) }}" method="POST" class="d-inline">
{{ form.csrf_token }}
<button type="submit" class="btn btn-danger" onclick="return confirm('¿Está seguro de que desea eliminar permanentemente este proyecto? Esta acción no se puede deshacer.');">
<i class="fas fa-trash-alt"></i> Eliminar Proyecto Permanentemente
</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ project.descripcion }} - ARCH{% endblock %} {% block title %}Proyecto: {{ project.descripcion }}{% endblock %}
{% block styles %} {% block styles %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/projects.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/projects.css') }}">
@ -10,175 +10,153 @@
{% block page_title %}Proyecto: {{ project.descripcion }}{% endblock %} {% block page_title %}Proyecto: {{ project.descripcion }}{% endblock %}
{% block content %} {% block content %}
<div class="row mb-4"> <div class="container mt-4">
<div class="col-md-8"> <div class="d-flex justify-content-between align-items-center mb-4">
<div class="card"> <h2>Detalles del Proyecto</h2>
<div>
<a href="{{ url_for('projects.edit', project_id=project.codigo) }}" class="btn btn-outline-primary me-1" title="Editar proyecto">
<i class="fas fa-edit fa-lg"></i>
</a>
<a href="{{ url_for('projects.list') }}" class="btn btn-outline-secondary" title="Volver a la lista">
<i class="fas fa-arrow-left fa-lg"></i>
</a>
</div>
</div>
<div class="card mb-4">
<div class="card-header bg-light">
<h3>{{ project.descripcion }}</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Código:</strong> {{ project.codigo }}</p>
<p><strong>Cliente:</strong> {{ project.cliente }}</p>
<p><strong>Destinación:</strong> {{ project.destinacion or 'No especificada' }}</p>
<p><strong>Estado:</strong> <span class="badge {{ 'bg-success' if project.estado == 'activo' else 'bg-secondary' }}">{{ project.estado|capitalize }}</span></p>
</div>
<div class="col-md-6">
<p><strong>Esquema:</strong> {{ project.esquema }}</p>
<p><strong>Proyecto Padre:</strong> {{ project.proyecto_padre or 'Ninguno' }}</p>
<p><strong>Año Creación:</strong> {{ project.ano_creacion }}</p>
<p><strong>Fecha Creación:</strong> {{ project.fecha_creacion|default('No disponible', true) }}</p>
</div>
</div>
<div class="row mt-3">
<div class="col">
<p><strong>Creado por:</strong> {{ project.creado_por }}</p>
<p><strong>Última modificación:</strong> {{ project.ultima_modificacion|default('No modificado', true) }}</p>
<p><strong>Modificado por:</strong> {{ project.modificado_por|default('No modificado', true) }}</p>
</div>
</div>
</div>
</div>
<!-- Proyectos Hijos -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Detalles del Proyecto</h5> <h4 class="mb-0">Proyectos Relacionados</h4>
<span class="badge bg-{{ 'success' if project.estado == 'activo' else 'danger' }}"> <a href="{{ url_for('projects.create') }}" class="btn btn-outline-success" title="Nuevo proyecto relacionado">
{{ project.estado|capitalize }} <i class="fas fa-plus fa-lg"></i>
</span> </a>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row">
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Código:</dt>
<dd class="col-sm-8">{{ project.codigo }}</dd>
<dt class="col-sm-4">Cliente:</dt>
<dd class="col-sm-8">{{ project.cliente }}</dd>
<dt class="col-sm-4">Destinación:</dt>
<dd class="col-sm-8">{{ project.destinacion or 'No especificada' }}</dd>
</dl>
</div>
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Año:</dt>
<dd class="col-sm-8">{{ project.ano_creacion }}</dd>
<dt class="col-sm-4">Creación:</dt>
<dd class="col-sm-8">{{ project.fecha_creacion|replace('T', ' ')|replace('Z', '') }}</dd>
<dt class="col-sm-4">Creado por:</dt>
<dd class="col-sm-8">{{ project.creado_por }}</dd>
</dl>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Proyecto padre:</dt>
<dd class="col-sm-8">
{% if project.proyecto_padre %}
<a href="{{ url_for('projects.view', project_id=project.proyecto_padre|replace('PROJ', '')|int) }}">
{{ project.proyecto_padre }}
</a>
{% else %}
<em>Ninguno</em>
{% endif %}
</dd>
<dt class="col-sm-4">Esquema:</dt>
<dd class="col-sm-8">{{ project.esquema }}</dd>
</dl>
</div>
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Última modificación:</dt>
<dd class="col-sm-8">{{ project.ultima_modificacion|replace('T', ' ')|replace('Z', '') }}</dd>
<dt class="col-sm-4">Modificado por:</dt>
<dd class="col-sm-8">{{ project.modificado_por }}</dd>
</dl>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-between">
<a href="{{ url_for('projects.list') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Volver a la lista
</a>
<div class="btn-group">
{% if current_user.has_permission(5000) and project.estado == 'activo' %}
<a href="{{ url_for('projects.edit', project_id=project.codigo|replace('PROJ', '')|int) }}"
class="btn btn-warning">
<i class="bi bi-pencil"></i> Editar Proyecto
</a>
{% endif %}
{% if current_user.has_permission(1000) and project.estado == 'activo' %}
<a href="{{ url_for('projects.create') }}?padre={{ project.codigo }}"
class="btn btn-info">
<i class="bi bi-diagram-3"></i> Crear Subproyecto
</a>
{% endif %}
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Información</h5>
</div>
<div class="card-body">
<p>
<strong>Documentos:</strong>
<span class="badge bg-info">{{ document_count }}</span>
</p>
<p>
<strong>Subproyectos:</strong>
<span class="badge bg-info">{{ children|length }}</span>
</p>
{% if project.estado == 'activo' and current_user.has_permission(1000) %}
<hr>
<div class="d-grid gap-2">
<a href="{{ url_for('documents.upload', project_id=project.codigo|replace('PROJ', '')|int) }}"
class="btn btn-success">
<i class="bi bi-upload"></i> Subir Documento
</a>
<a href="{{ url_for('documents.export', project_id=project.codigo|replace('PROJ', '')|int) }}"
class="btn btn-primary">
<i class="bi bi-box-arrow-down"></i> Exportar Proyecto
</a>
</div>
{% endif %}
</div>
</div>
{% if children %} {% if children %}
<div class="card mt-3"> <div class="table-responsive">
<div class="card-header"> <table class="table table-striped">
<h5 class="card-title mb-0">Subproyectos</h5> <thead>
</div> <tr>
<div class="card-body p-0"> <th>Código</th>
<div class="list-group list-group-flush"> <th>Descripción</th>
<th>Cliente</th>
<th>Estado</th>
<th width="100">Acciones</th>
</tr>
</thead>
<tbody>
{% for child in children %} {% for child in children %}
<a href="{{ url_for('projects.view', project_id=child.codigo|replace('PROJ', '')|int) }}" <tr>
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"> <td>{{ child.codigo }}</td>
{{ child.descripcion }} <td>{{ child.descripcion }}</td>
<span class="badge bg-{{ 'success' if child.estado == 'activo' else 'danger' }} rounded-pill"> <td>{{ child.cliente }}</td>
{{ child.estado|capitalize }} <td><span class="badge {{ 'bg-success' if child.estado == 'activo' else 'bg-secondary' }}">{{ child.estado|capitalize }}</span></td>
</span> <td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('projects.view', project_id=child.codigo) }}" class="btn btn-outline-info" title="Ver detalles">
<i class="fas fa-eye fa-lg"></i>
</a> </a>
<a href="{{ url_for('projects.edit', project_id=child.codigo) }}" class="btn btn-outline-primary" title="Editar">
<i class="fas fa-edit fa-lg"></i>
</a>
<button type="button" class="btn btn-outline-danger" title="Eliminar"
onclick="confirmDelete('{{ child.codigo }}', '{{ child.descripcion }}')">
<i class="fas fa-trash-alt fa-lg"></i>
</button>
</div>
</td>
</tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div> </div>
</div> {% else %}
</div> <p class="text-muted">No hay proyectos relacionados.</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<!-- Sección de documentos --> <!-- Documentos -->
<div class="row">
<div class="col-12">
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Documentos del Proyecto</h5> <h4 class="mb-0">Documentos ({{ document_count }})</h4>
<a href="{{ url_for('documents.list', project_id=project.codigo|replace('PROJ', '')|int) }}" <a href="{{ url_for('documents.upload', project_id=project.codigo) }}" class="btn btn-outline-success" title="Subir documento">
class="btn btn-sm btn-outline-primary"> <i class="fas fa-upload fa-lg"></i>
Ver todos
</a> </a>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="documents-container"> <div id="documents-container">
<div class="text-center py-5"> {% if document_count > 0 %}
<div class="text-center">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span> <span class="visually-hidden">Cargando...</span>
</div> </div>
<p class="mt-2">Cargando documentos...</p> <p>Cargando documentos...</p>
</div>
{% else %}
<p class="text-muted">No hay documentos en este proyecto.</p>
{% endif %}
</div>
{% if document_count > 0 %}
<div class="mt-3">
<a href="{{ url_for('documents.list', project_id=project.codigo) }}" class="btn btn-outline-primary">
<i class="fas fa-list fa-lg"></i> Ver todos los documentos
</a>
</div>
{% endif %}
</div> </div>
</div> </div>
</div>
<!-- Modal de confirmación para eliminar -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">Confirmar eliminación</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
¿Está seguro de que desea eliminar el proyecto <span id="projectToDelete"></span>?
<p class="text-danger mt-2">Esta acción no se puede deshacer.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form id="deleteForm" method="POST">
{{ csrf_token() }}
<button type="submit" class="btn btn-danger">Eliminar</button>
</form>
</div> </div>
</div> </div>
</div> </div>
@ -191,7 +169,7 @@
// Cargar documentos del proyecto mediante AJAX // Cargar documentos del proyecto mediante AJAX
$(document).ready(function() { $(document).ready(function() {
$.ajax({ $.ajax({
url: "{{ url_for('documents.api_list', project_id=project.codigo|replace('PROJ', '')|int) }}", url: "{{ url_for('documents.api_list', project_id=project.codigo) }}",
type: "GET", type: "GET",
dataType: "json", dataType: "json",
success: function(data) { success: function(data) {
@ -251,10 +229,10 @@
'<td>' + latestVersion.created_by + '</td>' + '<td>' + latestVersion.created_by + '</td>' +
'<td>' + '<td>' +
'<div class="btn-group btn-group-sm">' + '<div class="btn-group btn-group-sm">' +
'<a href="/documents/' + {{ project.codigo|replace('PROJ', '')|int }} + '/' + doc.id + '" class="btn btn-primary" title="Ver versiones">' + '<a href="/documents/' + project.codigo + '/' + doc.id + '" class="btn btn-primary" title="Ver versiones">' +
'<i class="bi bi-eye"></i>' + '<i class="bi bi-eye"></i>' +
'</a>' + '</a>' +
'<a href="/documents/' + {{ project.codigo|replace('PROJ', '')|int }} + '/' + doc.id + '/download/' + latestVersion.version + '" class="btn btn-success" title="Descargar última versión">' + '<a href="/documents/' + project.codigo + '/' + doc.id + '/download/' + latestVersion.version + '" class="btn btn-success" title="Descargar última versión">' +
'<i class="bi bi-download"></i>' + '<i class="bi bi-download"></i>' +
'</a>' + '</a>' +
'</div>' + '</div>' +
@ -266,5 +244,15 @@
$("#documents-container").html(html); $("#documents-container").html(html);
} }
// Configurar modal de eliminación
function confirmDelete(projectId, projectName) {
document.getElementById('projectToDelete').textContent = projectName;
document.getElementById('deleteForm').action = "{{ url_for('projects.delete', project_id='PLACEHOLDER') }}".replace('PLACEHOLDER', projectId);
// Mostrar modal usando Bootstrap
var myModal = new bootstrap.Modal(document.getElementById('deleteModal'));
myModal.show();
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,276 @@
{% extends "base.html" %}
{% block title %}Editar Esquema: {{ schema.nombre }}{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>Editar Esquema</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre</label>
<input type="text" class="form-control" id="nombre" name="nombre" value="{{ schema.nombre }}" required>
</div>
<div class="mb-3">
<label for="descripcion" class="form-label">Descripción</label>
<textarea class="form-control" id="descripcion" name="descripcion" rows="3">{{ schema.descripcion }}</textarea>
</div>
<h4 class="mt-4">Tipos de Documentos</h4>
<div id="documents-container">
{% for doc in schema.documentos %}
<div class="card mb-3 document-card">
<div class="card-body">
<div class="row">
<div class="col-md-3">
<label class="form-label">Tipo</label>
<select class="form-select" name="doc_tipo_{{ loop.index0 }}" required>
{% for extension, filetype in filetypes.items() %}
<option value="{{ extension }}" {% if doc.tipo == extension %}selected{% endif %}>
{{ filetype.descripcion }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input type="text" class="form-control" name="doc_nombre_{{ loop.index0 }}"
value="{{ doc.nombre }}" required>
</div>
<div class="col-md-2">
<label class="form-label">Nivel Ver</label>
<input type="number" class="form-control" name="doc_nivel_ver_{{ loop.index0 }}"
value="{{ doc.nivel_ver }}" min="0" max="9999">
</div>
<div class="col-md-2">
<label class="form-label">Nivel Editar</label>
<input type="number" class="form-control" name="doc_nivel_editar_{{ loop.index0 }}"
value="{{ doc.nivel_editar }}" min="0" max="9999">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm mt-4 remove-document">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<input type="hidden" id="document_count" name="document_count" value="{{ schema.documentos|length }}">
<button type="button" id="add-document" class="btn btn-secondary mb-3">
<i class="fas fa-plus"></i> Añadir Tipo de Documento
</button>
<h4 class="mt-4">Campos</h4>
<div id="fields-container">
{% for field in schema.fields %}
<div class="card mb-3 field-card">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input type="text" class="form-control" name="field_name_{{ loop.index0 }}" value="{{ field.name }}" required>
</div>
<div class="col-md-4">
<label class="form-label">Tipo</label>
<select class="form-select" name="field_type_{{ loop.index0 }}">
<option value="text" {% if field.type == 'text' %}selected{% endif %}>Texto</option>
<option value="textarea" {% if field.type == 'textarea' %}selected{% endif %}>Área de texto</option>
<option value="richtext" {% if field.type == 'richtext' %}selected{% endif %}>Texto enriquecido</option>
<option value="number" {% if field.type == 'number' %}selected{% endif %}>Número</option>
<option value="date" {% if field.type == 'date' %}selected{% endif %}>Fecha</option>
<option value="tags" {% if field.type == 'tags' %}selected{% endif %}>Etiquetas</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Requerido</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="field_required_{{ loop.index0 }}" {% if field.required %}checked{% endif %}>
<label class="form-check-label">Es requerido</label>
</div>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm mt-4 remove-field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<input type="hidden" id="field_count" name="field_count" value="{{ schema.fields|length }}">
<button type="button" id="add-field" class="btn btn-secondary mb-3">
<i class="fas fa-plus"></i> Añadir Campo
</button>
<div class="mt-4">
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
<a href="{{ url_for('schemas.list') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
let fieldCount = {{ schema.fields|length }};
// Add new field
document.getElementById('add-field').addEventListener('click', function() {
const fieldsContainer = document.getElementById('fields-container');
const newField = document.createElement('div');
newField.className = 'card mb-3 field-card';
newField.innerHTML = `
<div class="card-body">
<div class="row">
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input type="text" class="form-control" name="field_name_${fieldCount}" required>
</div>
<div class="col-md-4">
<label class="form-label">Tipo</label>
<select class="form-select" name="field_type_${fieldCount}">
<option value="text">Texto</option>
<option value="textarea">Área de texto</option>
<option value="richtext">Texto enriquecido</option>
<option value="number">Número</option>
<option value="date">Fecha</option>
<option value="tags">Etiquetas</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Requerido</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="field_required_${fieldCount}">
<label class="form-check-label">Es requerido</label>
</div>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm mt-4 remove-field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
fieldsContainer.appendChild(newField);
fieldCount++;
document.getElementById('field_count').value = fieldCount;
// Add event listener to new remove button
newField.querySelector('.remove-field').addEventListener('click', removeField);
});
// Remove field functionality
function removeField() {
this.closest('.field-card').remove();
fieldCount--;
document.getElementById('field_count').value = fieldCount;
// Renumber fields to keep proper sequence
updateFieldNumbers();
}
// Add event listeners to existing remove buttons
document.querySelectorAll('.remove-field').forEach(button => {
button.addEventListener('click', removeField);
});
// Update field numbering when fields are removed
function updateFieldNumbers() {
const fieldCards = document.querySelectorAll('.field-card');
fieldCards.forEach((card, index) => {
const fieldNameInput = card.querySelector('input[name^="field_name_"]');
const fieldTypeSelect = card.querySelector('select[name^="field_type_"]');
const fieldRequiredCheck = card.querySelector('input[name^="field_required_"]');
fieldNameInput.name = `field_name_${index}`;
fieldTypeSelect.name = `field_type_${index}`;
fieldRequiredCheck.name = `field_required_${index}`;
});
}
// Document types functionality
let documentCount = {{ schema.documentos|length }};
document.getElementById('add-document').addEventListener('click', function() {
const documentsContainer = document.getElementById('documents-container');
const newDocument = document.createElement('div');
newDocument.className = 'card mb-3 document-card';
newDocument.innerHTML = `
<div class="card-body">
<div class="row">
<div class="col-md-3">
<label class="form-label">Tipo</label>
<select class="form-select" name="doc_tipo_${documentCount}" required>
{% for extension, filetype in filetypes.items() %}
<option value="{{ extension }}">{{ filetype.descripcion }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input type="text" class="form-control" name="doc_nombre_${documentCount}" required>
</div>
<div class="col-md-2">
<label class="form-label">Nivel Ver</label>
<input type="number" class="form-control" name="doc_nivel_ver_${documentCount}"
value="0" min="0" max="9999">
</div>
<div class="col-md-2">
<label class="form-label">Nivel Editar</label>
<input type="number" class="form-control" name="doc_nivel_editar_${documentCount}"
value="5000" min="0" max="9999">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm mt-4 remove-document">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
documentsContainer.appendChild(newDocument);
documentCount++;
document.getElementById('document_count').value = documentCount;
newDocument.querySelector('.remove-document').addEventListener('click', removeDocument);
});
function removeDocument() {
this.closest('.document-card').remove();
documentCount--;
document.getElementById('document_count').value = documentCount;
updateDocumentNumbers();
}
document.querySelectorAll('.remove-document').forEach(button => {
button.addEventListener('click', removeDocument);
});
function updateDocumentNumbers() {
const documentCards = document.querySelectorAll('.document-card');
documentCards.forEach((card, index) => {
const tipoSelect = card.querySelector('select[name^="doc_tipo_"]');
const nombreInput = card.querySelector('input[name^="doc_nombre_"]');
const nivelVerInput = card.querySelector('input[name^="doc_nivel_ver_"]');
const nivelEditarInput = card.querySelector('input[name^="doc_nivel_editar_"]');
tipoSelect.name = `doc_tipo_${index}`;
nombreInput.name = `doc_nombre_${index}`;
nivelVerInput.name = `doc_nivel_ver_${index}`;
nivelEditarInput.name = `doc_nivel_editar_${index}`;
});
}
});
</script>
{% endblock %}
{% endblock %}

View File

@ -28,25 +28,25 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for schema_id, schema in schemas.items() %} {% for schema in schemas %}
<tr> <tr>
<td>{{ schema.codigo }}</td> <td>{{ schema.id }}</td>
<td>{{ schema.descripcion }}</td> <td>{{ schema.descripcion }}</td>
<td>{{ schema.documentos|length }}</td> <td>{{ schema.documentos|length }}</td>
<td>{{ schema.fecha_creacion|default('-') }}</td> <td>{{ schema.fecha_creacion|default('-') }}</td>
<td> <td>
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<a href="{{ url_for('schemas.view', schema_id=schema_id) }}" <a href="{{ url_for('schemas.view', schema_id=schema.id) }}"
class="btn btn-outline-primary" title="Ver detalles"> class="btn btn-outline-primary" title="Ver detalles">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
{% if current_user.has_permission(9000) %} {% if current_user.has_permission(9000) %}
<a href="{{ url_for('schemas.edit', schema_id=schema_id) }}" <a href="{{ url_for('schemas.edit_schema', schema_id=schema.id) }}"
class="btn btn-outline-secondary" title="Editar"> class="btn btn-outline-secondary" title="Editar">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<button type="button" class="btn btn-outline-danger" <button type="button" class="btn btn-outline-danger"
onclick="confirmDeleteSchema('{{ schema_id }}', '{{ schema.descripcion }}')" onclick="confirmDeleteSchema('{{ schema.id }}', '{{ schema.descripcion }}')"
title="Eliminar"> title="Eliminar">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>

View File

@ -1,21 +1,24 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ schema.codigo }} - Esquemas - ARCH{% endblock %} {% block title %}{{ schema.nombre }} - Esquemas - ARCH{% endblock %}
{% block content %} {% block content %}
<div class="container my-5"> <div class="container my-5">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('schemas.list') }}">Esquemas</a></li> <li class="breadcrumb-item"><a href="{{ url_for('schemas.list') }}">Esquemas</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ schema.codigo }}</li> <li class="breadcrumb-item active" aria-current="page">{{ schema.nombre }}</li>
</ol> </ol>
</nav> </nav>
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1>{{ schema.descripcion }}</h1> <div>
<h1>{{ schema.nombre }}</h1>
<p class="text-muted mb-0">{{ schema.descripcion }}</p>
</div>
{% if current_user.has_permission(9000) %} {% if current_user.has_permission(9000) %}
<div> <div>
<a href="{{ url_for('schemas.edit', schema_id=schema.codigo) }}" class="btn btn-primary"> <a href="{{ url_for('schemas.edit_schema', schema_id=schema.codigo) }}" class="btn btn-primary">
<i class="fas fa-edit"></i> Editar <i class="fas fa-edit"></i> Editar
</a> </a>
<button class="btn btn-danger" onclick="confirmDeleteSchema('{{ schema.codigo }}', '{{ schema.descripcion }}')"> <button class="btn btn-danger" onclick="confirmDeleteSchema('{{ schema.codigo }}', '{{ schema.descripcion }}')">

53
test/conftest.py Normal file
View File

@ -0,0 +1,53 @@
import pytest
import os
import sys
import tempfile
import shutil
import uuid
# Add the project directory to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
@pytest.fixture(scope="function")
def app():
"""Create and configure a Flask app for testing."""
from app import create_app
# Crear un directorio temporal único para cada prueba
test_id = str(uuid.uuid4())
test_storage_dir = tempfile.mkdtemp(prefix=f"arch_test_{test_id}_")
# Forzar modo testing
os.environ["FLASK_ENV"] = "testing"
os.environ["TESTING"] = "True"
# Crear la aplicación con configuración de prueba
app = create_app("testing")
# Asegurar que estamos en modo testing
app.config["TESTING"] = True
app.config["ENV"] = "testing"
app.config["STORAGE_PATH"] = test_storage_dir
# Create an application context
with app.app_context():
yield app
# Limpiar después de cada prueba
try:
shutil.rmtree(test_storage_dir, ignore_errors=True)
except Exception as e:
print(f"Error al eliminar directorio de prueba: {e}")
@pytest.fixture
def client(app):
"""A test client for the app."""
return app.test_client()
@pytest.fixture
def runner(app):
"""A test CLI runner for the app."""
return app.test_cli_runner()

509
test/test_app.py Normal file
View File

@ -0,0 +1,509 @@
import os
import sys
import json
import pytest
import logging
import uuid
import time
from flask_testing import TestCase
from unittest import mock
# Add the project directory to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from app import create_app, initialize_storage_structure
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,
find_project_directory,
archive_project,
)
class AppTestCase(TestCase):
def create_app(self):
# Configurar la aplicación para pruebas
os.environ["FLASK_ENV"] = "testing"
app = create_app("testing")
return app
def setUp(self):
# Inicializar la estructura de almacenamiento para pruebas
initialize_storage_structure(self.app)
# Create a test schema for projects
schemas_dir = os.path.join(self.app.config["STORAGE_PATH"], "schemas")
os.makedirs(schemas_dir, exist_ok=True)
with open(os.path.join(schemas_dir, "schema.json"), "w") as f:
json.dump(
{
"SCHEMA1": {
"name": "Test Schema",
"descripcion": "Schema for testing",
},
"SCHEMA2": {
"name": "Another Schema",
"descripcion": "Another schema for testing",
},
},
f,
)
# Generar un identificador único para esta sesión de prueba
self.test_session_id = str(uuid.uuid4())[:8]
def tearDown(self):
# Cerrar los manejadores de logging del app logger
for handler in self.app.logger.handlers[:]:
self.app.logger.removeHandler(handler)
handler.close()
# Limpiar después de cada prueba
storage_path = self.app.config["STORAGE_PATH"]
if os.path.exists(storage_path):
import shutil
import time
# Retry logic to handle file in use errors
for _ in range(5):
try:
shutil.rmtree(storage_path)
break
except PermissionError:
time.sleep(1)
def get_unique_name(self, base_name):
"""Genera un nombre único para evitar conflictos en pruebas"""
timestamp = int(time.time() * 1000) % 10000
return f"{base_name}_{self.test_session_id}_{timestamp}"
def clean_existing_projects(self):
"""Limpia proyectos existentes que puedan interferir con las pruebas"""
with self.app.app_context():
projects = get_all_projects(include_inactive=True)
for project in projects:
project_id = int(project["codigo"].replace("PROJ", ""))
delete_project(project_id)
def test_logging_setup(self):
# Verificar que el sistema de logging se haya configurado correctamente
log_path = os.path.join(self.app.config["STORAGE_PATH"], "logs")
assert os.path.exists(log_path), "El directorio de logs no existe"
def test_project_creation(self):
# Verificar la creación de un proyecto de prueba
project_path = os.path.join(
self.app.config["STORAGE_PATH"], "projects", "test_project"
)
os.makedirs(project_path, exist_ok=True)
assert os.path.exists(
project_path
), "El proyecto de prueba no se creó correctamente"
def test_project_service_creation(self):
"""Test creating a project using project service"""
with self.app.app_context():
# Limpiar proyectos existentes primero
self.clean_existing_projects()
project_data = {
"descripcion": self.get_unique_name("Test Project Service"),
"cliente": "Test Client",
"esquema": "SCHEMA1",
"destinacion": "Test Destination",
}
success, message, project_id = create_project(project_data, "test_user")
# Verify project creation success
assert success, f"Project creation failed: {message}"
assert project_id is not None
assert message == "Proyecto creado correctamente."
# Verify project directory was created with correct format
project_dir = find_project_directory(project_id)
assert project_dir is not None
assert os.path.exists(project_dir)
# Verify project metadata file was created
meta_file = os.path.join(project_dir, "project_meta.json")
assert os.path.exists(meta_file)
# Verify project metadata content
with open(meta_file, "r") as f:
metadata = json.load(f)
assert metadata["descripcion"] == "Test Project Service"
assert metadata["cliente"] == "Test Client"
assert metadata["esquema"] == "SCHEMA1"
assert metadata["creado_por"] == "test_user"
assert metadata["estado"] == "activo"
def test_project_retrieval(self):
"""Test retrieving project information"""
with self.app.app_context():
# Limpiar proyectos existentes primero
self.clean_existing_projects()
# First create a project
project_data = {
"descripcion": self.get_unique_name("Retrieval Test Project"),
"cliente": "Retrieval Client",
"esquema": "SCHEMA1",
}
success, _, project_id = create_project(project_data, "test_user")
assert success
# Now retrieve it
project = get_project(project_id)
# Verify retrieval
assert project is not None
assert project["descripcion"] == "Retrieval Test Project"
assert project["cliente"] == "Retrieval Client"
assert "directory" in project, "Directory information should be included"
def test_project_update(self):
"""Test updating project information"""
with self.app.app_context():
# Limpiar proyectos existentes primero
self.clean_existing_projects()
# First create a project
project_data = {
"descripcion": self.get_unique_name("Original Project"),
"cliente": "Original Client",
"esquema": "SCHEMA1",
}
success, _, project_id = create_project(project_data, "test_user")
assert success
# Update the project
updated_data = {
"descripcion": "Updated Project",
"cliente": "Updated Client",
"destinacion": "Updated Destination",
}
update_success, update_message = update_project(
project_id, updated_data, "modifier_user"
)
# Verify update success
assert update_success, f"Update failed: {update_message}"
assert update_message == "Proyecto actualizado correctamente."
# Verify the changes were applied
updated_project = get_project(project_id)
assert updated_project["descripcion"] == "Updated Project"
assert updated_project["cliente"] == "Updated Client"
assert updated_project["destinacion"] == "Updated Destination"
assert updated_project["modificado_por"] == "modifier_user"
# Schema should remain unchanged
assert updated_project["esquema"] == "SCHEMA1"
def test_project_deletion(self):
"""Test project soft deletion"""
with self.app.app_context():
# Limpiar proyectos existentes primero
self.clean_existing_projects()
# First create a project
project_data = {
"descripcion": self.get_unique_name("Project to Delete"),
"cliente": "Delete Client",
"esquema": "SCHEMA1",
}
success, _, project_id = create_project(project_data, "test_user")
assert success
# Delete the project (mark as inactive)
delete_success, delete_message = delete_project(project_id)
# Verify deletion success
assert delete_success, f"Deletion failed: {delete_message}"
assert delete_message == "Proyecto marcado como inactivo."
# Verify the project is marked as inactive
inactive_project = get_project(project_id)
assert inactive_project["estado"] == "inactivo"
# Verify the project directory still exists (soft delete)
project_dir = find_project_directory(project_id)
assert os.path.exists(project_dir)
def test_get_all_projects(self):
"""Test retrieving all projects"""
with self.app.app_context():
# Limpiar proyectos existentes primero
self.clean_existing_projects()
# Create multiple projects
projects_data = [
{
"descripcion": self.get_unique_name("Project 1"),
"cliente": "Client 1",
"esquema": "SCHEMA1",
},
{
"descripcion": self.get_unique_name("Project 2"),
"cliente": "Client 2",
"esquema": "SCHEMA1",
},
{
"descripcion": self.get_unique_name("Project 3"),
"cliente": "Client 1",
"esquema": "SCHEMA2",
},
]
project_ids = []
for data in projects_data:
success, _, project_id = create_project(data, "test_user")
assert success
project_ids.append(project_id)
# Mark one project as inactive
delete_project(project_ids[1])
# Get all active projects
all_active_projects = get_all_projects(include_inactive=False)
# Should only return 2 active projects
assert len(all_active_projects) == 2
# Get all projects including inactive
all_projects = get_all_projects(include_inactive=True)
# Should return all 3 projects
assert len(all_projects) == 3
def test_project_filtering(self):
"""Test project filtering functionality"""
with self.app.app_context():
# Limpiar proyectos existentes primero
self.clean_existing_projects()
# Create projects with different characteristics
projects_data = [
{
"descripcion": self.get_unique_name("Web Development"),
"cliente": "Client A",
"esquema": "SCHEMA1",
},
{
"descripcion": self.get_unique_name("Mobile App"),
"cliente": "Client B",
"esquema": "SCHEMA1",
},
{
"descripcion": self.get_unique_name("Desktop Application"),
"cliente": "Client A",
"esquema": "SCHEMA2",
},
]
for data in projects_data:
success, _, _ = create_project(data, "test_user")
assert success
# Test filtering by client
client_filter = {"cliente": "Client A"}
client_results = filter_projects(client_filter)
assert len(client_results) == 2
assert all(p["cliente"] == "Client A" for p in client_results)
# Test filtering by description
desc_filter = {"descripcion": "Web"}
desc_results = filter_projects(desc_filter)
assert len(desc_results) == 1
assert desc_results[0]["descripcion"] == "Web Development"
def test_project_hierarchy(self):
"""Test parent-child project relationships"""
with self.app.app_context():
# Limpiar proyectos existentes primero
self.clean_existing_projects()
# Create parent project
parent_data = {
"descripcion": self.get_unique_name("Parent Project"),
"cliente": "Parent Client",
"esquema": "SCHEMA1",
}
success, _, parent_id = create_project(parent_data, "test_user")
assert success
# Get the parent project code
parent_project = get_project(parent_id)
parent_code = parent_project["codigo"]
# Create child projects
child_data = [
{
"descripcion": "Child Project 1",
"cliente": "Child Client",
"esquema": "SCHEMA1",
"proyecto_padre": parent_code,
},
{
"descripcion": "Child Project 2",
"cliente": "Child Client",
"esquema": "SCHEMA1",
"proyecto_padre": parent_code,
},
]
for data in child_data:
success, _, _ = create_project(data, "test_user")
assert success
# Get children of parent project
children = get_project_children(parent_id)
# Verify children
assert len(children) == 2
child_descriptions = [child["descripcion"] for child in children]
assert "Child Project 1" in child_descriptions
assert "Child Project 2" in child_descriptions
def test_document_count(self):
"""Test counting documents in a project"""
with self.app.app_context():
# Limpiar proyectos existentes primero
self.clean_existing_projects()
# Create a test project
project_data = {
"descripcion": self.get_unique_name("Document Test Project"),
"cliente": "Document Client",
"esquema": "SCHEMA1",
}
success, _, project_id = create_project(project_data, "test_user")
assert success
# Find project directory
project_dir = find_project_directory(project_id)
documents_dir = os.path.join(project_dir, "documents")
# Initially should have no documents
initial_count = get_project_document_count(project_id)
assert initial_count == 0
# Create some mock document directories
os.makedirs(os.path.join(documents_dir, "@001_doc1"), exist_ok=True)
os.makedirs(os.path.join(documents_dir, "@002_doc2"), exist_ok=True)
# Create a non-document directory (should be ignored)
os.makedirs(os.path.join(documents_dir, "not_a_document"), exist_ok=True)
# Count should now be 2
updated_count = get_project_document_count(project_id)
assert updated_count == 2
def test_project_routes(self):
"""Test project routes and API endpoints"""
with self.app.app_context():
# Test project list route
response = self.client.get("/projects/")
self.assertStatus(
response, 302
) # Cambiar a esperar redirección en lugar de 200
# Test project API list route
api_response = self.client.get("/projects/api/list")
self.assert200(api_response)
data = json.loads(api_response.data)
assert isinstance(data, list)
# Test project creation route (GET - form)
create_response = self.client.get("/projects/create")
self.assert200(create_response)
# Setup mock for testing POST with CSRF token
with mock.patch("flask_wtf.csrf.validate_csrf", return_value=True):
# Test project creation route (POST)
post_data = {
"descripcion": "Route Test Project",
"cliente": "Route Test Client",
"esquema": "SCHEMA1",
"destinacion": "Route Test Destination",
"proyecto_padre": "",
}
# Mock project_service.create_project to return success
with mock.patch(
"routes.project_routes.create_project",
return_value=(True, "Proyecto creado correctamente.", 1),
):
post_response = self.client.post("/projects/create", data=post_data)
self.assertStatus(post_response, 302) # Redirect on success
# Test project view route
# First create a real project to view
project_data = {
"descripcion": "View Test Project",
"cliente": "View Client",
"esquema": "SCHEMA1",
}
success, _, project_id = create_project(project_data, "test_user")
assert success
view_response = self.client.get(f"/projects/{project_id}")
self.assert200(view_response)
def test_project_archival(self):
"""Test project archival functionality"""
with self.app.app_context():
# Limpiar proyectos existentes primero
self.clean_existing_projects()
# First create a project
project_data = {
"descripcion": self.get_unique_name("Project to Archive"),
"cliente": "Archive Client",
"esquema": "SCHEMA1",
}
success, _, project_id = create_project(project_data, "test_user")
assert success
# Archive the project
archive_success, archive_message = archive_project(
project_id, "archiver_user"
)
# Verify archival success
assert archive_success, f"Archival failed: {archive_message}"
assert archive_message == "Proyecto archivado correctamente."
# Verify the project is marked as archived
archived_project = get_project(project_id)
assert archived_project["estado"] == "archivado"
# Verify the project directory still exists (soft archive)
project_dir = find_project_directory(project_id)
assert os.path.exists(project_dir)
if __name__ == "__main__":
# Ejecutar pruebas y exportar resultados a resultados_test.json
result = pytest.main(
[
"-v",
"--tb=short",
"--disable-warnings",
"--json-report",
"--json-report-file=resultados_test.json",
]
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +0,0 @@
{
"max_project_id": 0,
"max_document_id": 0
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ001",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Updated Project",
"cliente": "Updated Client",
"destinacion": "Updated Destination",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:12.284860+00:00",
"creado_por": "test_user",
"estado": "inactivo",
"ultima_modificacion": "2025-03-05T12:31:08.662224+00:00",
"modificado_por": "modifier_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ001",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Original Project_fe1bbfd7_8658",
"cliente": "Original Client",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:31:08.659457+00:00",
"creado_por": "test_user",
"estado": "activo",
"ultima_modificacion": "2025-03-05T12:31:08.659457+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ001",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Parent Project_b8be6fbe_8152",
"cliente": "Parent Client",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:48.153922+00:00",
"creado_por": "test_user",
"estado": "activo",
"ultima_modificacion": "2025-03-05T12:30:48.153922+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ001",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Project 1_bfd95ace_7355",
"cliente": "Client 1",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:17.355896+00:00",
"creado_por": "test_user",
"estado": "activo",
"ultima_modificacion": "2025-03-05T12:30:17.355896+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ001",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Project to Archive_3351c50e_7691",
"cliente": "Archive Client",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:27.691603+00:00",
"creado_por": "test_user",
"estado": "activo",
"ultima_modificacion": "2025-03-05T12:30:27.691603+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ001",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Project to Delete_512cbeb3_7876",
"cliente": "Delete Client",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:37.878750+00:00",
"creado_por": "test_user",
"estado": "activo",
"ultima_modificacion": "2025-03-05T12:30:37.878750+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ001",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Retrieval Test Project_f6a719e6_3290",
"cliente": "Retrieval Client",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:53.290000+00:00",
"creado_por": "test_user",
"estado": "activo",
"ultima_modificacion": "2025-03-05T12:30:53.290000+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ001",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Test Project Service_7a112033_3510",
"cliente": "Test Client",
"destinacion": "Test Destination",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:31:03.511752+00:00",
"creado_por": "test_user",
"estado": "activo",
"ultima_modificacion": "2025-03-05T12:31:03.511752+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ001",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Web Development_dbd130b6_2990",
"cliente": "Client A",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:42.991449+00:00",
"creado_por": "test_user",
"estado": "activo",
"ultima_modificacion": "2025-03-05T12:30:42.991449+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ002",
"proyecto_padre": "PROJ001",
"esquema": "SCHEMA1",
"descripcion": "Child Project 1",
"cliente": "Child Client",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:48.157907+00:00",
"creado_por": "test_user",
"estado": "inactivo",
"ultima_modificacion": "2025-03-05T12:31:08.657465+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ002",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Mobile App_dbd130b6_2990",
"cliente": "Client B",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:42.995446+00:00",
"creado_por": "test_user",
"estado": "inactivo",
"ultima_modificacion": "2025-03-05T12:30:48.151905+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ002",
"proyecto_padre": null,
"esquema": "SCHEMA1",
"descripcion": "Project 2_bfd95ace_7355",
"cliente": "Client 2",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:17.357869+00:00",
"creado_por": "test_user",
"estado": "inactivo",
"ultima_modificacion": "2025-03-05T12:30:42.980488+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ003",
"proyecto_padre": "PROJ001",
"esquema": "SCHEMA1",
"descripcion": "Child Project 2",
"cliente": "Child Client",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:48.160909+00:00",
"creado_por": "test_user",
"estado": "inactivo",
"ultima_modificacion": "2025-03-05T12:31:08.658462+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Test Schema",
"descripcion": "Schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ003",
"proyecto_padre": null,
"esquema": "SCHEMA2",
"descripcion": "Desktop Application_dbd130b6_2990",
"cliente": "Client A",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:43.005400+00:00",
"creado_por": "test_user",
"estado": "inactivo",
"ultima_modificacion": "2025-03-05T12:30:48.152885+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Another Schema",
"descripcion": "Another schema for testing"
}

View File

@ -0,0 +1,14 @@
{
"codigo": "PROJ003",
"proyecto_padre": null,
"esquema": "SCHEMA2",
"descripcion": "Project 3_bfd95ace_7355",
"cliente": "Client 1",
"destinacion": "",
"ano_creacion": 2025,
"fecha_creacion": "2025-03-05T12:30:17.361848+00:00",
"creado_por": "test_user",
"estado": "inactivo",
"ultima_modificacion": "2025-03-05T12:30:42.989417+00:00",
"modificado_por": "test_user"
}

View File

@ -0,0 +1,4 @@
{
"name": "Another Schema",
"descripcion": "Another schema for testing"
}

View File

@ -1,28 +1 @@
{ {"SCHEMA1": {"name": "Test Schema", "descripcion": "Schema for testing"}, "SCHEMA2": {"name": "Another Schema", "descripcion": "Another schema for testing"}}
"ESQ001": {
"codigo": "ESQ001",
"descripcion": "Proyecto estándar",
"fecha_creacion": "2025-03-04T10:25:03.799581+00:00",
"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
}
]
}
}

View File

@ -3,12 +3,12 @@
"nombre": "Administrador", "nombre": "Administrador",
"username": "admin", "username": "admin",
"email": "admin@example.com", "email": "admin@example.com",
"password_hash": "$2b$04$UGmPF4BBNejhS7ZKA58/hu3A8DtXkx7/Fyw.8igRby6eDw/RwkvAq", "password_hash": "$2b$04$SuOrsLdaXHIG85Ni/Jw9sOBbVfE/ImINWLhywDn4/sHZfTiFeXQX2",
"nivel": 9999, "nivel": 9999,
"idioma": "es", "idioma": "es",
"fecha_caducidad": null, "fecha_caducidad": null,
"empresa": "", "empresa": "",
"estado": "activo", "estado": "activo",
"ultimo_acceso": "2025-03-04T10:25:03.799581+00:00" "ultimo_acceso": "2025-03-05T12:30:12.274854+00:00"
} }
} }

View File

@ -1,2 +0,0 @@
# This file makes the tests directory a Python package
# allowing the json_reporter module to be imported

View File

@ -1,205 +0,0 @@
import pytest
import os
import json
import shutil
import time
import tempfile
from app import create_app
def init_test_environment(storage_path):
"""Initialize the test environment with required directories."""
if os.path.exists(storage_path):
try:
shutil.rmtree(storage_path)
except PermissionError:
# Try to handle Windows file locking issues
print(f"Warning: Could not remove {storage_path} directory, retrying...")
time.sleep(1)
try:
shutil.rmtree(storage_path)
except Exception as e:
print(f"Failed to remove directory: {e}")
# Create main storage directory
os.makedirs(storage_path, exist_ok=True)
# Create required subdirectories
for dir_name in ["users", "schemas", "filetypes", "projects", "logs"]:
os.makedirs(os.path.join(storage_path, dir_name), exist_ok=True)
# Initialize test data
initialize_test_data(storage_path)
@pytest.fixture
def app():
# Use a unique temporary directory for each test session
storage_path = tempfile.mkdtemp(prefix="arch_test_")
# Set up the test environment
init_test_environment(storage_path)
# Create app with test configuration
app = create_app("testing")
# Update the app's config with our test settings
app.config.update(
{
"TESTING": True,
"STORAGE_PATH": storage_path,
"WTF_CSRF_ENABLED": False, # Disable CSRF protection for testing
"SERVER_NAME": "localhost.localdomain", # Set server name to ensure correct URLs
}
)
# Create app context
with app.app_context():
yield app
# Clean up after tests - with retry mechanism for Windows
max_retries = 3
retry_delay = 1
for attempt in range(max_retries):
try:
if os.path.exists(storage_path):
shutil.rmtree(storage_path)
break
except PermissionError:
if attempt < max_retries - 1:
print(
f"Cleanup attempt {attempt+1} failed, retrying in {retry_delay}s..."
)
time.sleep(retry_delay)
else:
print(f"Warning: Could not clean up {storage_path}")
@pytest.fixture
def client(app):
"""Create a test client for the application."""
with app.test_client() as client:
# Enable cookies and sessions for the test client
client.testing = True
yield client
@pytest.fixture
def auth(client):
"""Helper for authentication tests."""
class AuthActions:
def login(self, username="admin", password="admin123"):
# Use session for better cookie handling
with client.session_transaction() as session:
# Pre-clear any existing session data
session.clear()
response = client.post(
"/auth/login",
data={"username": username, "password": password},
follow_redirects=True,
)
return response
def logout(self):
return client.get("/auth/logout", follow_redirects=True)
return AuthActions()
@pytest.fixture
def logged_in_client(client, auth):
"""Client that's already logged in as admin."""
response = auth.login()
# Verify login was successful
assert response.status_code == 200
# Check for expected content in response to confirm logged in state
assert (
b"Cerrar" in response.data
or b"Logout" in response.data
or b"Panel" in response.data
)
return client
def initialize_test_data(storage_path):
"""Initialize test data files."""
# Create test users
users_data = {
"admin": {
"nombre": "Administrador",
"username": "admin",
"email": "admin@ejemplo.com",
"password_hash": "$2b$12$Q5Nz3QSF0FP.mKAxPmWXmurKn1oor4Cl1KbYZAKsFbGcEWWyPHou6", # admin123
"nivel": 9999,
"idioma": "es",
"fecha_caducidad": None,
"empresa": "ARCH",
"estado": "activo",
},
"user1": {
"nombre": "Usuario Normal",
"username": "user1",
"email": "user1@ejemplo.com",
"password_hash": "$2b$12$Q5Nz3QSF0FP.mKAxPmWXmurKn1oor4Cl1KbYZAKsFbGcEWWyPHou6", # admin123
"nivel": 1000,
"idioma": "es",
"fecha_caducidad": None,
"empresa": "ARCH",
"estado": "activo",
},
}
with open(f"{storage_path}/users/users.json", "w", encoding="utf-8") as f:
json.dump(users_data, f, ensure_ascii=False, indent=2)
# Create test file types
filetypes_data = {
"pdf": {
"extension": "pdf",
"descripcion": "Documento PDF",
"mime_type": "application/pdf",
"tamano_maximo": 20971520,
},
"txt": {
"extension": "txt",
"descripcion": "Documento de texto",
"mime_type": "text/plain",
"tamano_maximo": 5242880,
},
}
with open(f"{storage_path}/filetypes/filetypes.json", "w", encoding="utf-8") as f:
json.dump(filetypes_data, f, ensure_ascii=False, indent=2)
# Create test schemas
schemas_data = {
"TEST001": {
"codigo": "TEST001",
"descripcion": "Esquema de prueba",
"fecha_creacion": "2023-10-01T10:00:00Z",
"creado_por": "admin",
"documentos": [
{
"tipo": "pdf",
"nombre": "Documento de Prueba",
"nivel_ver": 0,
"nivel_editar": 1000,
}
],
}
}
with open(f"{storage_path}/schemas/schema.json", "w", encoding="utf-8") as f:
json.dump(schemas_data, f, ensure_ascii=False, indent=2)
# Create indices file
indices_data = {"max_project_id": 0, "max_document_id": 0}
with open(f"{storage_path}/indices.json", "w", encoding="utf-8") as f:
json.dump(indices_data, f, ensure_ascii=False, indent=2)

View File

@ -1,15 +0,0 @@
"""Helper functions for tests."""
import os
def ensure_clean_session(client):
"""Ensure we have a clean session before each test."""
# Get the root URL to reset application state
with client.session_transaction() as session:
session.clear()
# Make a request to reset client state
client.get("/")
return client

View File

@ -1,108 +0,0 @@
import json
import pytest
import datetime
import os
import time
from pathlib import Path
class JSONReporter:
"""
Custom pytest plugin to generate JSON test reports.
Based on the specification in section 9.4 of descripcion.md.
"""
def __init__(self, config):
self.config = config
self.start_time = time.time()
self.results = {
"summary": {
"total": 0,
"passed": 0,
"failed": 0,
"skipped": 0,
"error": 0,
"duration": 0,
"timestamp": datetime.datetime.now().isoformat(),
},
"tests": [],
}
def pytest_runtest_logreport(self, report):
"""Handle the reporting of a test run."""
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:
if report.when != "call":
result = "error"
self.results["summary"]["error"] += 1
else:
result = "failed"
self.results["summary"]["failed"] += 1
elif report.skipped:
result = "skipped"
self.results["summary"]["skipped"] += 1
# Extract test metadata
test_module = report.nodeid.split("::")[0]
test_class = report.nodeid.split("::")[1] if "::" in report.nodeid else None
test_name = report.nodeid.split("::")[-1]
# Extract error details if present
error_message = None
error_trace = None
if hasattr(report, "longrepr") and report.longrepr:
if hasattr(report.longrepr, "reprcrash") and report.longrepr.reprcrash:
error_message = report.longrepr.reprcrash.message
error_trace = str(report.longrepr)
# Add test result to list
self.results["tests"].append(
{
"id": report.nodeid,
"module": test_module,
"class": test_class,
"name": test_name,
"result": result,
"duration": report.duration,
"error_message": error_message,
"error_trace": error_trace,
}
)
def pytest_sessionfinish(self, session):
"""Generate report at end of test session."""
self.results["summary"]["duration"] = time.time() - self.start_time
# Create output directory if it doesn't exist
output_dir = Path("test_reports")
output_dir.mkdir(exist_ok=True)
# Generate timestamp for file name
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = output_dir / f"test_results_{timestamp}.json"
# Write results to file
with open(output_path, "w", encoding="utf-8") as f:
json.dump(self.results, f, indent=2, ensure_ascii=False)
# Also create a symlink/copy to latest results
latest_path = output_dir / "test_results_latest.json"
if os.path.exists(latest_path):
os.remove(latest_path)
with open(latest_path, "w", encoding="utf-8") as f:
json.dump(self.results, f, indent=2, ensure_ascii=False)
print(f"\nJSON test report generated: {output_path}")
# Register the plugin
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
"""Register the JSON reporter plugin."""
config._json_reporter = JSONReporter(config)
config.pluginmanager.register(config._json_reporter, "json_reporter")

View File

@ -1,132 +0,0 @@
import pytest
import json
import os
from .helpers import ensure_clean_session
class TestAdminFunctions:
"""Test administrative functions."""
def test_admin_dashboard(self, logged_in_client):
"""Test accessing admin dashboard."""
# Ensure we have a clean, authenticated session
logged_in_client = ensure_clean_session(logged_in_client)
# Access the admin dashboard
response = logged_in_client.get("/admin/dashboard")
assert response.status_code == 200
assert b"Administraci" in response.data or b"Dashboard" in response.data
def test_filetypes_management(self, logged_in_client):
"""Test file types management page."""
response = logged_in_client.get("/admin/filetypes")
assert response.status_code == 200
assert b"Tipos de Archivo" in response.data
def test_system_status(self, logged_in_client):
"""Test system status page."""
response = logged_in_client.get("/admin/system")
assert response.status_code == 200
assert b"Estado del Sistema" in response.data
def test_add_filetype(self, logged_in_client, app):
"""Test adding a new file type."""
response = logged_in_client.post(
"/admin/filetypes/add",
data={
"extension": "docx",
"descripcion": "Documento Word",
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"tamano_maximo": 15728640, # 15MB in bytes
},
follow_redirects=True,
)
assert response.status_code == 200
# Verify file type was added
with app.app_context():
filetypes_path = os.path.join(
app.config["STORAGE_PATH"], "filetypes", "filetypes.json"
)
with open(filetypes_path, "r") as f:
filetypes = json.load(f)
assert "docx" in filetypes
assert filetypes["docx"]["descripcion"] == "Documento Word"
def test_delete_filetype(self, logged_in_client, app):
"""Test deleting a file type."""
# First add a file type to delete
logged_in_client.post(
"/admin/filetypes/add",
data={
"extension": "xlsx",
"descripcion": "Hoja de cálculo Excel",
"mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"tamano_maximo": 15728640,
},
follow_redirects=True,
)
# Verify it was added
with app.app_context():
filetypes_path = os.path.join(
app.config["STORAGE_PATH"], "filetypes", "filetypes.json"
)
with open(filetypes_path, "r") as f:
filetypes = json.load(f)
assert "xlsx" in filetypes
# Now delete it
response = logged_in_client.post(
"/admin/filetypes/xlsx/delete", follow_redirects=True
)
assert response.status_code == 200
# Verify it was deleted
with app.app_context():
filetypes_path = os.path.join(
app.config["STORAGE_PATH"], "filetypes", "filetypes.json"
)
with open(filetypes_path, "r") as f:
filetypes = json.load(f)
assert "xlsx" not in filetypes
def test_update_filetype(self, logged_in_client, app):
"""Test updating a file type."""
# First add a file type to update
logged_in_client.post(
"/admin/filetypes/add",
data={
"extension": "png",
"descripcion": "Imagen PNG",
"mime_type": "image/png",
"tamano_maximo": 5242880, # 5MB
},
follow_redirects=True,
)
# Now update it
response = logged_in_client.post(
"/admin/filetypes/png/update",
data={
"descripcion": "Imagen PNG Actualizada",
"mime_type": "image/png",
"tamano_maximo": 10485760, # 10MB (increased)
},
follow_redirects=True,
)
assert response.status_code == 200
# Verify changes
with app.app_context():
filetypes_path = os.path.join(
app.config["STORAGE_PATH"], "filetypes", "filetypes.json"
)
with open(filetypes_path, "r") as f:
filetypes = json.load(f)
assert "png" in filetypes
assert filetypes["png"]["descripcion"] == "Imagen PNG Actualizada"
assert filetypes["png"]["tamano_maximo"] == 10485760

View File

@ -1,72 +0,0 @@
import pytest
from flask import session, g
class TestAuth:
"""Test authentication functionality."""
def test_login_page(self, client):
"""Test that login page loads correctly."""
response = client.get("/auth/login")
assert response.status_code == 200
assert b"Iniciar sesi" in response.data # 'Iniciar sesión' in Spanish
def test_login_success(self, client):
"""Test successful login with correct credentials."""
response = client.post(
"/auth/login",
data={"username": "admin", "password": "admin123"},
follow_redirects=True,
)
assert response.status_code == 200
# Check that we're redirected to the right page after login
assert b"Panel" in response.data or b"Proyectos" in response.data
def test_login_invalid_credentials(self, client):
"""Test login with invalid credentials."""
response = client.post(
"/auth/login",
data={"username": "admin", "password": "wrongpassword"},
follow_redirects=True,
)
assert response.status_code == 200
assert (
b"credenciales" in response.data.lower()
) # Error message about credentials
def test_logout(self, auth, client):
"""Test logout functionality."""
# First login
auth.login()
# Then logout
response = auth.logout()
assert response.status_code == 200
# Check if logged out - try to access a protected page
response = client.get("/users/", follow_redirects=True)
assert b"iniciar sesi" in response.data.lower() # Should see login page
def test_access_protected_route(self, client):
"""Test accessing a protected route without login."""
# Try to access users list without login
response = client.get("/users/", follow_redirects=True)
assert response.status_code == 200
assert b"iniciar sesi" in response.data.lower() # Should be redirected to login
def test_access_protected_route_with_login(self, logged_in_client):
"""Test accessing a protected route with login."""
# Admin should be able to access users list
response = logged_in_client.get("/admin/dashboard")
assert response.status_code == 200
def test_permission_levels(self, client, auth):
"""Test different permission levels."""
# Login as regular user
auth.login(username="user1", password="admin123")
# Try to access admin-only page
response = client.get("/admin/dashboard", follow_redirects=True)
assert (
response.status_code == 403 or b"acceso denegado" in response.data.lower()
)

View File

@ -1,156 +0,0 @@
import pytest
import os
import io
import json
from werkzeug.datastructures import FileStorage
class TestDocuments:
"""Test document management functionality."""
def setup_project(self, logged_in_client, app):
"""Helper to create a test project."""
# Create a project
logged_in_client.post(
"/projects/create",
data={
"codigo": "TESTDOC",
"descripcion": "Proyecto para documentos",
"cliente": "Cliente Test",
"esquema": "TEST001",
"destinacion": "Pruebas",
"notas": "Proyecto para pruebas de documentos",
},
follow_redirects=True,
)
# Find the project ID
with app.app_context():
projects_dir = os.path.join(app.config["STORAGE_PATH"], "projects")
project_dirs = [d for d in os.listdir(projects_dir) if "TESTDOC" in d]
assert len(project_dirs) > 0
project_dir = project_dirs[0]
return project_dir.split("_")[0].replace("@", "")
def test_upload_document(self, logged_in_client, app):
"""Test uploading a document to a project."""
# Create a project and get its ID
project_id = self.setup_project(logged_in_client, app)
# Create a test file
test_file = FileStorage(
stream=io.BytesIO(b"This is a test document content."),
filename="test_document.txt",
content_type="text/plain",
)
# Upload the document
response = logged_in_client.post(
f"/projects/{project_id}/documents/upload",
data={
"tipo_doc": "Documento de Prueba", # Match schema type from TEST001
"descripcion": "Documento de prueba para tests",
"file": test_file,
},
content_type="multipart/form-data",
follow_redirects=True,
)
assert response.status_code == 200
# Verify document was created
with app.app_context():
project_path = None
projects_dir = os.path.join(app.config["STORAGE_PATH"], "projects")
for dir_name in os.listdir(projects_dir):
if dir_name.startswith(f"@{project_id}_"):
project_path = os.path.join(projects_dir, dir_name)
break
assert project_path is not None
# Check for documents directory with content
docs_path = os.path.join(project_path, "documents")
assert os.path.exists(docs_path)
# Should have at least one document folder
assert len(os.listdir(docs_path)) > 0
def test_document_versions(self, logged_in_client, app):
"""Test document versioning functionality."""
# Create a project and upload first version
project_id = self.setup_project(logged_in_client, app)
# Upload first version
test_file1 = FileStorage(
stream=io.BytesIO(b"Document content version 1"),
filename="test_versioning.txt",
content_type="text/plain",
)
logged_in_client.post(
f"/projects/{project_id}/documents/upload",
data={
"tipo_doc": "Documento de Prueba",
"descripcion": "Documento para pruebas de versiones",
"file": test_file1,
},
content_type="multipart/form-data",
follow_redirects=True,
)
# Find the document ID
doc_id = None
with app.app_context():
projects_dir = os.path.join(app.config["STORAGE_PATH"], "projects")
for dir_name in os.listdir(projects_dir):
if dir_name.startswith(f"@{project_id}_"):
project_path = os.path.join(projects_dir, dir_name)
docs_path = os.path.join(project_path, "documents")
# Get first document directory
doc_dirs = os.listdir(docs_path)
if doc_dirs:
doc_id = doc_dirs[0].split("_")[0].replace("@", "")
break
assert doc_id is not None
# Upload second version of the same document
test_file2 = FileStorage(
stream=io.BytesIO(b"Document content version 2 - UPDATED"),
filename="test_versioning_v2.txt",
content_type="text/plain",
)
response = logged_in_client.post(
f"/projects/{project_id}/documents/{doc_id}/upload",
data={"descripcion": "Segunda versión del documento", "file": test_file2},
content_type="multipart/form-data",
follow_redirects=True,
)
assert response.status_code == 200
# Check for multiple versions in metadata
with app.app_context():
projects_dir = os.path.join(app.config["STORAGE_PATH"], "projects")
for dir_name in os.listdir(projects_dir):
if dir_name.startswith(f"@{project_id}_"):
project_path = os.path.join(projects_dir, dir_name)
docs_path = os.path.join(project_path, "documents")
# Find document directory
for doc_dir in os.listdir(docs_path):
if doc_dir.startswith(f"@{doc_id}_"):
doc_path = os.path.join(docs_path, doc_dir)
meta_path = os.path.join(doc_path, "meta.json")
# Check metadata for versions
if os.path.exists(meta_path):
with open(meta_path, "r") as f:
metadata = json.load(f)
assert "versions" in metadata
assert len(metadata["versions"]) >= 2

View File

@ -1,179 +0,0 @@
import pytest
import os
import io
from werkzeug.datastructures import FileStorage
class TestIntegration:
"""Test integrations between different components."""
def test_complete_workflow(self, logged_in_client, app):
"""Test a complete workflow from project creation to document download."""
# 1. Create a new project
logged_in_client.post(
"/projects/create",
data={
"codigo": "WORKFLOW",
"descripcion": "Proyecto de flujo completo",
"cliente": "Cliente Integración",
"esquema": "TEST001",
"destinacion": "Pruebas de integración",
"notas": "Notas de proyecto de prueba",
},
follow_redirects=True,
)
# 2. Find the project ID
project_id = None
with app.app_context():
projects_dir = os.path.join(app.config["STORAGE_PATH"], "projects")
project_dirs = [d for d in os.listdir(projects_dir) if "WORKFLOW" in d]
assert len(project_dirs) > 0
project_dir = project_dirs[0]
project_id = project_dir.split("_")[0].replace("@", "")
assert project_id is not None
# 3. Upload a document to the project
test_file = FileStorage(
stream=io.BytesIO(b"Content for integration test document"),
filename="integration_doc.txt",
content_type="text/plain",
)
response = logged_in_client.post(
f"/projects/{project_id}/documents/upload",
data={
"tipo_doc": "Documento de Prueba",
"descripcion": "Documento de flujo de integración",
"file": test_file,
},
content_type="multipart/form-data",
follow_redirects=True,
)
assert response.status_code == 200
# 4. Find the document ID
doc_id = None
with app.app_context():
projects_dir = os.path.join(app.config["STORAGE_PATH"], "projects")
for dir_name in os.listdir(projects_dir):
if dir_name.startswith(f"@{project_id}_"):
project_path = os.path.join(projects_dir, dir_name)
docs_path = os.path.join(project_path, "documents")
doc_dirs = os.listdir(docs_path)
if doc_dirs:
doc_id = doc_dirs[0].split("_")[0].replace("@", "")
break
assert doc_id is not None
# 5. View document details
response = logged_in_client.get(f"/projects/{project_id}/documents/{doc_id}")
assert response.status_code == 200
assert b"Documento de flujo de integraci" in response.data
# 6. Upload a new version of the document
test_file2 = FileStorage(
stream=io.BytesIO(b"Updated content for version 2"),
filename="integration_doc_v2.txt",
content_type="text/plain",
)
response = logged_in_client.post(
f"/projects/{project_id}/documents/{doc_id}/upload",
data={
"descripcion": "Segunda versión del documento de integración",
"file": test_file2,
},
content_type="multipart/form-data",
follow_redirects=True,
)
assert response.status_code == 200
# 7. Download the document
response = logged_in_client.get(
f"/projects/{project_id}/documents/{doc_id}/download/latest"
)
assert response.status_code == 200
assert b"Updated content for version 2" in response.data
# 8. Download a specific version (the first one)
response = logged_in_client.get(
f"/projects/{project_id}/documents/{doc_id}/download/1"
)
assert response.status_code == 200
assert b"Content for integration test document" in response.data
def test_schema_project_document_integration(self, logged_in_client, app):
"""Test integration between schemas, projects and documents."""
# 1. Create a custom schema
logged_in_client.post(
"/schemas/create",
data={
"codigo": "CUSTOM",
"descripcion": "Esquema personalizado para integración",
"documentos-0-tipo": "pdf",
"documentos-0-nombre": "Informe Principal",
"documentos-0-nivel_ver": 0,
"documentos-0-nivel_editar": 5000,
"documentos-1-tipo": "txt",
"documentos-1-nombre": "Notas Adicionales",
"documentos-1-nivel_ver": 0,
"documentos-1-nivel_editar": 1000,
},
follow_redirects=True,
)
# 2. Create a project with the custom schema
logged_in_client.post(
"/projects/create",
data={
"codigo": "PROJ_SCHEMA",
"descripcion": "Proyecto con esquema personalizado",
"cliente": "Cliente Test",
"esquema": "CUSTOM",
"destinacion": "Pruebas",
"notas": "Proyecto para probar integración de esquemas",
},
follow_redirects=True,
)
# 3. Find the project ID
project_id = None
with app.app_context():
projects_dir = os.path.join(app.config["STORAGE_PATH"], "projects")
project_dirs = [d for d in os.listdir(projects_dir) if "PROJ_SCHEMA" in d]
assert len(project_dirs) > 0
project_dir = project_dirs[0]
project_id = project_dir.split("_")[0].replace("@", "")
# 4. Verify project uses the custom schema
response = logged_in_client.get(f"/projects/{project_id}")
assert response.status_code == 200
assert b"CUSTOM" in response.data
assert b"Informe Principal" in response.data
assert b"Notas Adicionales" in response.data
# 5. Upload a document of the specified type
test_file = FileStorage(
stream=io.BytesIO(b"Notes content for schema integration"),
filename="notes.txt",
content_type="text/plain",
)
response = logged_in_client.post(
f"/projects/{project_id}/documents/upload",
data={
"tipo_doc": "Notas Adicionales", # This should match schema document type
"descripcion": "Notas para prueba de esquema",
"file": test_file,
},
content_type="multipart/form-data",
follow_redirects=True,
)
assert response.status_code == 200

View File

@ -1,110 +0,0 @@
import pytest
import json
import os
class TestProjects:
"""Test project-related functionality."""
def test_project_list(self, logged_in_client):
"""Test listing projects."""
response = logged_in_client.get("/projects/")
assert response.status_code == 200
def test_create_project(self, logged_in_client, app):
"""Test creating a new project."""
response = logged_in_client.post(
"/projects/create",
data={
"codigo": "TEST123",
"descripcion": "Proyecto de Prueba",
"cliente": "Cliente Test",
"esquema": "TEST001",
"destinacion": "Pruebas",
"notas": "Notas de prueba",
},
follow_redirects=True,
)
assert response.status_code == 200
# Check if project was created in storage
with app.app_context():
# Check indices
indices_path = os.path.join(app.config["STORAGE_PATH"], "indices.json")
with open(indices_path, "r") as f:
indices = json.load(f)
assert indices["max_project_id"] > 0
def test_view_project(self, logged_in_client, app):
"""Test viewing a project (requires creating one first)."""
# Create a project first
logged_in_client.post(
"/projects/create",
data={
"codigo": "TEST456",
"descripcion": "Proyecto para visualizar",
"cliente": "Cliente Test",
"esquema": "TEST001",
"destinacion": "Pruebas",
"notas": "Notas de prueba",
},
follow_redirects=True,
)
# Now find the project ID
with app.app_context():
projects_dir = os.path.join(app.config["STORAGE_PATH"], "projects")
project_dirs = os.listdir(projects_dir)
assert len(project_dirs) > 0
# Get first project directory
project_dir = project_dirs[0]
project_id = project_dir.split("_")[0].replace("@", "")
# Try to view it
response = logged_in_client.get(f"/projects/{project_id}")
assert response.status_code == 200
assert b"Proyecto para visualizar" in response.data
def test_edit_project(self, logged_in_client, app):
"""Test editing a project."""
# Create a project first
logged_in_client.post(
"/projects/create",
data={
"codigo": "TESTEDIT",
"descripcion": "Proyecto para editar",
"cliente": "Cliente Test",
"esquema": "TEST001",
"destinacion": "Pruebas",
"notas": "Notas originales",
},
follow_redirects=True,
)
# Find the project ID
with app.app_context():
projects_dir = os.path.join(app.config["STORAGE_PATH"], "projects")
project_dirs = [d for d in os.listdir(projects_dir) if "TESTEDIT" in d]
assert len(project_dirs) > 0
project_dir = project_dirs[0]
project_id = project_dir.split("_")[0].replace("@", "")
# Edit the project
response = logged_in_client.post(
f"/projects/{project_id}/edit",
data={
"codigo": "TESTEDIT",
"descripcion": "Proyecto editado",
"cliente": "Cliente Test Modificado",
"esquema": "TEST001",
"destinacion": "Pruebas Modificadas",
"notas": "Notas modificadas",
},
follow_redirects=True,
)
assert response.status_code == 200
assert b"Proyecto editado" in response.data

View File

@ -1,98 +0,0 @@
import pytest
import json
import os
class TestSchemas:
"""Test schema management functionality."""
def test_list_schemas(self, logged_in_client):
"""Test listing schemas."""
response = logged_in_client.get("/schemas/")
assert response.status_code == 200
assert b"Esquemas de Proyecto" in response.data
def test_view_schema(self, logged_in_client):
"""Test viewing a schema."""
response = logged_in_client.get("/schemas/view/TEST001")
assert response.status_code == 200
assert b"Esquema de prueba" in response.data
def test_create_schema(self, logged_in_client, app):
"""Test creating a new schema."""
response = logged_in_client.post(
"/schemas/create",
data={
"codigo": "TEST002",
"descripcion": "Nuevo esquema de prueba",
"documentos-0-tipo": "pdf",
"documentos-0-nombre": "Manual de Usuario",
"documentos-0-nivel_ver": 0,
"documentos-0-nivel_editar": 5000,
"documentos-1-tipo": "txt",
"documentos-1-nombre": "Notas de Proyecto",
"documentos-1-nivel_ver": 0,
"documentos-1-nivel_editar": 1000,
},
follow_redirects=True,
)
assert response.status_code == 200
# Check if schema was created
with app.app_context():
schemas_path = os.path.join(
app.config["STORAGE_PATH"], "schemas", "schema.json"
)
with open(schemas_path, "r") as f:
schemas = json.load(f)
assert "TEST002" in schemas
assert schemas["TEST002"]["descripcion"] == "Nuevo esquema de prueba"
assert len(schemas["TEST002"]["documentos"]) == 2
def test_edit_schema(self, logged_in_client, app):
"""Test editing a schema."""
# First create a schema to edit
logged_in_client.post(
"/schemas/create",
data={
"codigo": "TESTEDIT",
"descripcion": "Esquema para editar",
"documentos-0-tipo": "pdf",
"documentos-0-nombre": "Documento Original",
"documentos-0-nivel_ver": 0,
"documentos-0-nivel_editar": 5000,
},
follow_redirects=True,
)
# Now edit it
response = logged_in_client.post(
"/schemas/edit/TESTEDIT",
data={
"codigo": "TESTEDIT",
"descripcion": "Esquema editado",
"documentos-0-tipo": "pdf",
"documentos-0-nombre": "Documento Modificado",
"documentos-0-nivel_ver": 500,
"documentos-0-nivel_editar": 6000,
},
follow_redirects=True,
)
assert response.status_code == 200
# Verify changes
with app.app_context():
schemas_path = os.path.join(
app.config["STORAGE_PATH"], "schemas", "schema.json"
)
with open(schemas_path, "r") as f:
schemas = json.load(f)
assert "TESTEDIT" in schemas
assert schemas["TESTEDIT"]["descripcion"] == "Esquema editado"
assert (
schemas["TESTEDIT"]["documentos"][0]["nombre"]
== "Documento Modificado"
)
assert schemas["TESTEDIT"]["documentos"][0]["nivel_ver"] == 500

View File

@ -1,147 +0,0 @@
import pytest
import json
import os
class TestUserManagement:
"""Test user management functionality."""
def test_list_users(self, logged_in_client):
"""Test listing users."""
response = logged_in_client.get("/users/")
assert response.status_code == 200
assert b"Usuarios del Sistema" in response.data
# Check for existing users
assert b"admin" in response.data
assert b"user1" in response.data
def test_create_user(self, logged_in_client, app):
"""Test creating a new user."""
response = logged_in_client.post(
"/users/create",
data={
"nombre": "Usuario de Prueba",
"username": "testuser",
"email": "test@example.com",
"password": "password123",
"password_confirm": "password123",
"nivel": 1000,
"idioma": "es",
"empresa": "Empresa Test",
"estado": "activo",
"fecha_caducidad": "",
},
follow_redirects=True,
)
assert response.status_code == 200
# Check if user was created
with app.app_context():
users_path = os.path.join(app.config["STORAGE_PATH"], "users", "users.json")
with open(users_path, "r") as f:
users = json.load(f)
assert "testuser" in users
assert users["testuser"]["nombre"] == "Usuario de Prueba"
assert users["testuser"]["email"] == "test@example.com"
assert users["testuser"]["nivel"] == 1000
def test_edit_user(self, logged_in_client, app):
"""Test editing an existing user."""
# First create a user to edit
logged_in_client.post(
"/users/create",
data={
"nombre": "Usuario para Editar",
"username": "edit_user",
"email": "edit@example.com",
"password": "password123",
"password_confirm": "password123",
"nivel": 1000,
"idioma": "es",
"empresa": "Empresa Original",
"estado": "activo",
"fecha_caducidad": "",
},
follow_redirects=True,
)
# Now edit the user
response = logged_in_client.post(
"/users/edit/edit_user",
data={
"nombre": "Usuario Editado",
"email": "edited@example.com",
"password": "", # Empty password means no change
"password_confirm": "",
"nivel": 5000, # Changed level
"idioma": "en", # Changed language
"empresa": "Empresa Modificada",
"estado": "activo",
"fecha_caducidad": "",
},
follow_redirects=True,
)
assert response.status_code == 200
# Verify changes
with app.app_context():
users_path = os.path.join(app.config["STORAGE_PATH"], "users", "users.json")
with open(users_path, "r") as f:
users = json.load(f)
assert "edit_user" in users
assert users["edit_user"]["nombre"] == "Usuario Editado"
assert users["edit_user"]["email"] == "edited@example.com"
assert users["edit_user"]["nivel"] == 5000
assert users["edit_user"]["idioma"] == "en"
assert users["edit_user"]["empresa"] == "Empresa Modificada"
def test_delete_user(self, logged_in_client, app):
"""Test deleting a user."""
# First create a user to delete
logged_in_client.post(
"/users/create",
data={
"nombre": "Usuario para Eliminar",
"username": "delete_user",
"email": "delete@example.com",
"password": "password123",
"password_confirm": "password123",
"nivel": 1000,
"idioma": "es",
"empresa": "Empresa Test",
"estado": "activo",
"fecha_caducidad": "",
},
follow_redirects=True,
)
# Verify user was created
with app.app_context():
users_path = os.path.join(app.config["STORAGE_PATH"], "users", "users.json")
with open(users_path, "r") as f:
users = json.load(f)
assert "delete_user" in users
# Now delete the user
response = logged_in_client.post(
"/users/delete/delete_user", follow_redirects=True
)
assert response.status_code == 200
# Verify user was deleted
with app.app_context():
users_path = os.path.join(app.config["STORAGE_PATH"], "users", "users.json")
with open(users_path, "r") as f:
users = json.load(f)
assert "delete_user" not in users
def test_cannot_delete_admin(self, logged_in_client):
"""Test that admin user cannot be deleted."""
response = logged_in_client.post("/users/delete/admin", follow_redirects=True)
assert response.status_code == 200
# Should see an error message
assert b"No se puede eliminar" in response.data or b"no puede" in response.data