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

22
app.py
View File

@ -23,12 +23,24 @@ cache = Cache()
def create_app(config_name=None):
"""Fábrica de aplicación Flask."""
if config_name is None:
config_name = os.environ.get("FLASK_ENV", "default")
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
app.config["ENV"] = config_name # Añade esta línea si no existe
# Debugging para ver qué está pasando
print(f"Configurando aplicación en modo: {config_name}")
print(f"ENV actual: {app.config['ENV']}")
if isinstance(config_name, dict):
app.config.update(config_name)
else:
if config_name is None:
config_name = os.environ.get("FLASK_ENV", "default")
app.config.from_object(config[config_name])
config[config_name].init_app(app)
# Asegurar que el SECRET_KEY está configurado
if not app.config.get("SECRET_KEY"):
app.config["SECRET_KEY"] = "default_secret_key_please_change_in_production"
# Inicializar extensiones
login_manager.init_app(app)

View File

@ -103,7 +103,8 @@ La aplicación sigue una arquitectura basada en archivos JSON y sistema de fiche
{
"ESQ001": {
"codigo": "ESQ001",
"descripcion": "Proyecto estándar",
"nombre": "Siemens",
"descripcion": "Proyecto estándar Siemens",
"fecha_creacion": "2023-05-10T10:00:00Z",
"creado_por": "admin",
"documentos": [
@ -553,130 +554,6 @@ Se implementará un plugin personalizado para pytest que generará reportes en f
- Cobertura de código
- Detalles de errores encontrados
### 9.5 Ejemplo de Configuración de Pytest
```python
# conftest.py
import pytest
import os
import json
import shutil
from app import create_app
@pytest.fixture
def app():
"""Crear una instancia de la aplicación para pruebas."""
# Configuración de prueba
test_config = {
'TESTING': True,
'STORAGE_PATH': 'test_storage',
'SECRET_KEY': 'test_key'
}
# Crear directorio de almacenamiento para pruebas
if not os.path.exists('test_storage'):
os.makedirs('test_storage')
# Crear estructura básica
for dir in ['users', 'schemas', 'filetypes', 'projects', 'logs']:
if not os.path.exists(f'test_storage/{dir}'):
os.makedirs(f'test_storage/{dir}')
# Crear app con configuración de prueba
app = create_app(test_config)
yield app
# Limpiar después de las pruebas
shutil.rmtree('test_storage')
@pytest.fixture
def client(app):
"""Cliente de prueba para la aplicación."""
return app.test_client()
@pytest.fixture
def auth(client):
"""Helper para pruebas de autenticación."""
class AuthActions:
def login(self, username='admin', password='password'):
return client.post('/login', data={
'username': username,
'password': password
}, follow_redirects=True)
def logout(self):
return client.get('/logout', follow_redirects=True)
return AuthActions()
```
```python
# json_reporter.py
import json
import pytest
import datetime
import os
class JSONReporter:
def __init__(self, config):
self.config = config
self.results = {
'summary': {
'total': 0,
'passed': 0,
'failed': 0,
'skipped': 0,
'duration': 0,
'timestamp': datetime.datetime.now().isoformat()
},
'tests': []
}
def pytest_runtest_logreport(self, report):
if report.when == 'call' or (report.when == 'setup' and report.skipped):
self.results['summary']['total'] += 1
if report.passed:
result = 'passed'
self.results['summary']['passed'] += 1
elif report.failed:
result = 'failed'
self.results['summary']['failed'] += 1
else:
result = 'skipped'
self.results['summary']['skipped'] += 1
self.results['tests'].append({
'name': report.nodeid,
'result': result,
'duration': report.duration,
'error': str(report.longrepr) if hasattr(report, 'longrepr') and report.longrepr else None
})
def pytest_sessionfinish(self, session):
self.results['summary']['duration'] = session.config.hook.pytest_report_teststatus.get_duration()
with open('test_results.json', 'w') as f:
json.dump(self.results, f, indent=2)
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
config.pluginmanager.register(JSONReporter(config), 'json_reporter')
```
## 10. Guía de Implementación
### 10.1 Configuración del Entorno
1. Crear entorno virtual:
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
```
2. Instalar dependencias:
```bash
@ -747,6 +624,7 @@ filetypes = {
schemas = {
"ESQ001": {
"codigo": "ESQ001",
"nombre": "Siemens",
"descripcion": "Proyecto estándar",
"fecha_creacion": "2023-05-10T10:00:00Z",
"creado_por": "admin",

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

View File

@ -5,181 +5,235 @@ from wtforms import StringField, SelectField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
from services.project_service import (
create_project, update_project, get_project, delete_project,
get_all_projects, get_project_children, get_project_document_count,
filter_projects
create_project,
update_project,
get_project,
delete_project,
get_all_projects,
get_project_children,
get_project_document_count,
filter_projects,
archive_project,
)
from services.schema_service import get_all_schemas
from utils.security import permission_required
# Definir Blueprint
projects_bp = Blueprint('projects', __name__, url_prefix='/projects')
projects_bp = Blueprint("projects", __name__, url_prefix="/projects")
# Formularios
class ProjectForm(FlaskForm):
"""Formulario de proyecto."""
descripcion = StringField('Descripción', validators=[DataRequired(), Length(1, 100)])
cliente = StringField('Cliente', validators=[DataRequired(), Length(1, 100)])
destinacion = StringField('Destinación', validators=[Length(0, 100)])
esquema = SelectField('Esquema', validators=[DataRequired()])
proyecto_padre = SelectField('Proyecto Padre', validators=[])
submit = SubmitField('Guardar')
descripcion = StringField(
"Descripción", validators=[DataRequired(), Length(1, 100)]
)
cliente = StringField("Cliente", validators=[DataRequired(), Length(1, 100)])
destinacion = StringField("Destinación", validators=[Length(0, 100)])
esquema = SelectField("Esquema", validators=[DataRequired()])
proyecto_padre = SelectField("Proyecto Padre", validators=[])
submit = SubmitField("Guardar")
class ProjectFilterForm(FlaskForm):
"""Formulario de filtrado de proyectos."""
cliente = StringField('Cliente')
estado = SelectField('Estado', choices=[('', 'Todos'), ('activo', 'Activo'), ('inactivo', 'Inactivo')])
ano_inicio = StringField('Año Inicio')
ano_fin = StringField('Año Fin')
descripcion = StringField('Descripción')
submit = SubmitField('Filtrar')
cliente = StringField("Cliente")
estado = SelectField(
"Estado",
choices=[("", "Todos"), ("activo", "Activo"), ("inactivo", "Inactivo")],
)
ano_inicio = StringField("Año Inicio")
ano_fin = StringField("Año Fin")
descripcion = StringField("Descripción")
submit = SubmitField("Filtrar")
# Rutas
@projects_bp.route('/')
@projects_bp.route("/")
@login_required
def list():
"""Listar proyectos."""
filter_form = ProjectFilterForm(request.args)
# Obtener proyectos según filtros
if request.args:
projects = filter_projects(request.args)
else:
projects = get_all_projects()
return render_template('projects/list.html',
projects=projects,
filter_form=filter_form)
@projects_bp.route('/create', methods=['GET', 'POST'])
return render_template(
"projects/list.html", projects=projects, filter_form=filter_form
)
@projects_bp.route("/create", methods=["GET", "POST"])
@login_required
@permission_required(1000) # Nivel mínimo para crear proyectos
def create():
"""Crear nuevo proyecto."""
form = ProjectForm()
# Cargar opciones para esquemas
schemas = get_all_schemas()
form.esquema.choices = [(code, schema['descripcion']) for code, schema in schemas.items()]
form.esquema.choices = [
(schema["id"], schema.get("name", "") or schema.get("descripcion", ""))
for schema in schemas
]
# Cargar opciones para proyectos padre
projects = [(p['codigo'], p['descripcion']) for p in get_all_projects()]
form.proyecto_padre.choices = [('', 'Ninguno')] + projects
projects = [(p["codigo"], p["descripcion"]) for p in get_all_projects()]
form.proyecto_padre.choices = [("", "Ninguno")] + projects
if form.validate_on_submit():
# Preparar datos del proyecto
project_data = {
'descripcion': form.descripcion.data,
'cliente': form.cliente.data,
'destinacion': form.destinacion.data,
'esquema': form.esquema.data,
'proyecto_padre': form.proyecto_padre.data if form.proyecto_padre.data else None
"descripcion": form.descripcion.data,
"cliente": form.cliente.data,
"destinacion": form.destinacion.data,
"esquema": form.esquema.data,
"proyecto_padre": (
form.proyecto_padre.data if form.proyecto_padre.data else None
),
}
# Crear proyecto
success, message, project_id = create_project(project_data, current_user.username)
if success:
flash(message, 'success')
return redirect(url_for('projects.view', project_id=project_id))
else:
flash(message, 'danger')
return render_template('projects/create.html', form=form)
@projects_bp.route('/<int:project_id>')
# Crear proyecto
success, message, project_id = create_project(
project_data, current_user.username
)
if success:
flash(message, "success")
return redirect(url_for("projects.view", project_id=project_id))
else:
flash(message, "danger")
return render_template("projects/create.html", form=form, schemas=schemas)
@projects_bp.route("/<project_id>")
@login_required
def view(project_id):
"""Ver detalles de un proyecto."""
project = get_project(project_id)
if not project:
flash('Proyecto no encontrado.', 'danger')
return redirect(url_for('projects.list'))
flash("Proyecto no encontrado.", "danger")
return redirect(url_for("projects.list"))
# Obtener información adicional
children = get_project_children(project_id)
document_count = get_project_document_count(project_id)
return render_template('projects/view.html',
project=project,
children=children,
document_count=document_count)
@projects_bp.route('/<int:project_id>/edit', methods=['GET', 'POST'])
return render_template(
"projects/view.html",
project=project,
children=children,
document_count=document_count,
)
@projects_bp.route("/<project_id>/edit", methods=["GET", "POST"])
@login_required
@permission_required(5000) # Nivel mínimo para editar proyectos
def edit(project_id):
"""Editar un proyecto."""
project = get_project(project_id)
if not project:
flash('Proyecto no encontrado.', 'danger')
return redirect(url_for('projects.list'))
flash("Proyecto no encontrado.", "danger")
return redirect(url_for("projects.list"))
form = ProjectForm()
# Cargar opciones para esquemas
schemas = get_all_schemas()
form.esquema.choices = [(code, schema['descripcion']) for code, schema in schemas.items()]
form.esquema.choices = [
(schema["id"], schema.get("name", "") or schema.get("descripcion", ""))
for schema in schemas
]
# Cargar opciones para proyectos padre (excluyendo este proyecto y sus hijos)
all_projects = get_all_projects()
children_codes = [child['codigo'] for child in get_project_children(project_id)]
available_projects = [(p['codigo'], p['descripcion']) for p in all_projects
if p['codigo'] != project['codigo'] and p['codigo'] not in children_codes]
form.proyecto_padre.choices = [('', 'Ninguno')] + available_projects
if request.method == 'GET':
children_codes = [child["codigo"] for child in get_project_children(project_id)]
available_projects = [
(p["codigo"], p["descripcion"])
for p in all_projects
if p["codigo"] != project["codigo"] and p["codigo"] not in children_codes
]
form.proyecto_padre.choices = [("", "Ninguno")] + available_projects
if request.method == "GET":
# Cargar datos actuales
form.descripcion.data = project['descripcion']
form.cliente.data = project['cliente']
form.destinacion.data = project.get('destinacion', '')
form.esquema.data = project['esquema']
form.proyecto_padre.data = project.get('proyecto_padre', '')
form.descripcion.data = project["descripcion"]
form.cliente.data = project["cliente"]
form.destinacion.data = project.get("destinacion", "")
form.esquema.data = project["esquema"]
form.proyecto_padre.data = project.get("proyecto_padre", "")
if form.validate_on_submit():
# Preparar datos actualizados
project_data = {
'descripcion': form.descripcion.data,
'cliente': form.cliente.data,
'destinacion': form.destinacion.data,
'esquema': form.esquema.data,
'proyecto_padre': form.proyecto_padre.data if form.proyecto_padre.data else None
"descripcion": form.descripcion.data,
"cliente": form.cliente.data,
"destinacion": form.destinacion.data,
"esquema": form.esquema.data,
"proyecto_padre": (
form.proyecto_padre.data if form.proyecto_padre.data else None
),
}
# Actualizar proyecto
success, message = update_project(project_id, project_data, current_user.username)
if success:
flash(message, 'success')
return redirect(url_for('projects.view', project_id=project_id))
else:
flash(message, 'danger')
return render_template('projects/edit.html', form=form, project=project)
@projects_bp.route('/<int:project_id>/delete', methods=['POST'])
# Actualizar proyecto
success, message = update_project(
project_id, project_data, current_user.username
)
if success:
flash(message, "success")
return redirect(url_for("projects.view", project_id=project_id))
else:
flash(message, "danger")
return render_template(
"projects/edit.html", form=form, project=project, schemas=schemas
)
@projects_bp.route("/<project_id>/delete", methods=["POST"])
@login_required
@permission_required(9000) # Nivel alto para eliminar proyectos
def delete(project_id):
"""Eliminar un proyecto (marcar como inactivo)."""
success, message = delete_project(project_id)
if success:
flash(message, 'success')
else:
flash(message, 'danger')
return redirect(url_for('projects.list'))
@projects_bp.route('/api/list')
if success:
flash(message, "success")
else:
flash(message, "danger")
return redirect(url_for("projects.list"))
@projects_bp.route("/<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
def api_list():
"""API para listar proyectos (para selects dinámicos)."""
projects = get_all_projects()
return jsonify([{
'id': p['codigo'],
'text': p['descripcion']
} for p in projects])
return jsonify([{"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_wtf import FlaskForm
from wtforms import (
@ -106,43 +115,39 @@ def create():
@schemas_bp.route("/edit/<schema_id>", methods=["GET", "POST"])
@login_required
@permission_required(9000) # Solo administradores
def edit(schema_id):
"""Editar esquema existente."""
def edit_schema(schema_id):
schema = get_schema_by_id(schema_id)
if not schema:
flash("Esquema no encontrado.", "danger")
flash("Esquema no encontrado", "danger")
return redirect(url_for("schemas.list"))
# Prepopulate form
form = SchemaForm(obj=schema)
filetypes = get_allowed_filetypes()
if form.validate_on_submit():
data = {
"codigo": form.codigo.data,
"descripcion": form.descripcion.data,
"documentos": [],
if request.method == "POST":
# Extract form data
nombre = request.form.get("nombre")
descripcion = request.form.get("descripcion")
# Update schema data
updated_schema = {
"id": schema_id,
"nombre": nombre,
"descripcion": descripcion,
"created_by": schema.get("created_by"),
"created_at": schema.get("created_at"),
"documentos": schema.get("documentos", []),
}
for doc_form in form.documentos:
data["documentos"].append(
{
"tipo": doc_form.tipo.data,
"nombre": doc_form.nombre.data,
"nivel_ver": doc_form.nivel_ver.data,
"nivel_editar": doc_form.nivel_editar.data,
}
)
success, message = update_schema(schema_id, data)
success = update_schema(schema_id, updated_schema)
if success:
flash(message, "success")
flash("Esquema actualizado correctamente", "success")
return redirect(url_for("schemas.list"))
else:
flash(message, "danger")
flash("Error al actualizar el esquema", "danger")
return render_template("schemas/edit.html", form=form, schema=schema)
return render_template("schemas/edit.html", schema=schema, filetypes=filetypes)
@schemas_bp.route("/delete/<schema_id>", methods=["POST"])
@ -177,7 +182,7 @@ def api_list():
@login_required
def api_get(schema_code):
"""API para obtener un esquema específico."""
schema = get_schema(schema_code)
schema = get_schema_by_id(schema_code)
if not schema:
return jsonify({"error": "Esquema no encontrado"}), 404

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,434 +7,454 @@ from werkzeug.utils import secure_filename
from flask import current_app
from utils.file_utils import (
load_json_file, save_json_file, ensure_dir_exists,
get_next_id, format_document_directory_name,
format_version_filename
load_json_file,
save_json_file,
ensure_dir_exists,
get_next_id,
format_document_directory_name,
format_version_filename,
)
from utils.security import calculate_checksum, check_file_type
from services.project_service import find_project_directory
def get_allowed_filetypes():
"""
Obtener los tipos de archivo permitidos.
Returns:
dict: Diccionario de tipos de archivo
"""
storage_path = current_app.config['STORAGE_PATH']
filetypes_file = os.path.join(storage_path, 'filetypes', 'filetypes.json')
return load_json_file(filetypes_file, {})
"""Get all allowed filetypes from storage."""
storage_path = current_app.config["STORAGE_PATH"]
filetypes_path = os.path.join(storage_path, "filetypes", "filetypes.json")
if os.path.exists(filetypes_path):
with open(filetypes_path, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def add_document(project_id, document_data, file, creator_username):
"""
Añadir un nuevo documento a un proyecto.
Args:
project_id (int): ID del proyecto
document_data (dict): Datos del documento
file: Objeto de archivo (de Flask)
creator_username (str): Usuario que crea el documento
Returns:
tuple: (success, message, document_id)
"""
# Buscar directorio del proyecto
project_dir = find_project_directory(project_id)
if not project_dir:
return False, f"No se encontró el proyecto con ID {project_id}.", None
# Validar datos obligatorios
if 'nombre' not in document_data or not document_data['nombre']:
if "nombre" not in document_data or not document_data["nombre"]:
return False, "El nombre del documento es obligatorio.", None
# Validar tipo de archivo
allowed_filetypes = get_allowed_filetypes()
filename = secure_filename(file.filename)
extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
extension = filename.rsplit(".", 1)[1].lower() if "." in filename else ""
if extension not in allowed_filetypes:
return False, f"Tipo de archivo no permitido: {extension}", None
# Verificar MIME type
if not check_file_type(file.stream, [allowed_filetypes[extension]['mime_type']]):
if not check_file_type(file.stream, [allowed_filetypes[extension]["mime_type"]]):
return False, "El tipo de archivo no coincide con su extensión.", None
# Obtener siguiente ID de documento
document_id = get_next_id('document')
document_id = get_next_id("document")
# Preparar directorio del documento
documents_dir = os.path.join(project_dir, 'documents')
dir_name = format_document_directory_name(document_id, document_data['nombre'])
documents_dir = os.path.join(project_dir, "documents")
dir_name = format_document_directory_name(document_id, document_data["nombre"])
document_dir = os.path.join(documents_dir, dir_name)
# Verificar si ya existe
if os.path.exists(document_dir):
return False, "Ya existe un documento con ese nombre en este proyecto.", None
# Crear directorio
ensure_dir_exists(document_dir)
# Preparar primera versión
version = 1
version_filename = format_version_filename(version, document_data['nombre'], extension)
version_filename = format_version_filename(
version, document_data["nombre"], extension
)
version_path = os.path.join(document_dir, version_filename)
# Guardar archivo
file.seek(0)
file.save(version_path)
# Calcular checksum
checksum = calculate_checksum(version_path)
# Obtener tamaño del archivo
file_size = os.path.getsize(version_path)
# Preparar metadatos del documento
document_meta = {
'document_id': f"{document_id:03d}_{document_data['nombre'].lower().replace(' ', '_')}",
'original_filename': filename,
'versions': [
"document_id": f"{document_id:03d}_{document_data['nombre'].lower().replace(' ', '_')}",
"original_filename": filename,
"versions": [
{
'version': version,
'filename': version_filename,
'created_at': datetime.now(pytz.UTC).isoformat(),
'created_by': creator_username,
'description': document_data.get('description', 'Versión inicial'),
'file_size': file_size,
'mime_type': allowed_filetypes[extension]['mime_type'],
'checksum': checksum,
'downloads': []
"version": version,
"filename": version_filename,
"created_at": datetime.now(pytz.UTC).isoformat(),
"created_by": creator_username,
"description": document_data.get("description", "Versión inicial"),
"file_size": file_size,
"mime_type": allowed_filetypes[extension]["mime_type"],
"checksum": checksum,
"downloads": [],
}
]
],
}
# Guardar metadatos
meta_file = os.path.join(document_dir, 'meta.json')
meta_file = os.path.join(document_dir, "meta.json")
save_json_file(meta_file, document_meta)
return True, "Documento añadido correctamente.", document_id
def add_version(project_id, document_id, version_data, file, creator_username):
"""
Añadir una nueva versión a un documento existente.
Args:
project_id (int): ID del proyecto
document_id (int): ID del documento
version_data (dict): Datos de la versión
file: Objeto de archivo (de Flask)
creator_username (str): Usuario que crea la versión
Returns:
tuple: (success, message, version_number)
"""
# Buscar directorio del proyecto
project_dir = find_project_directory(project_id)
if not project_dir:
return False, f"No se encontró el proyecto con ID {project_id}.", None
# Buscar documento
document_dir = find_document_directory(project_dir, document_id)
if not document_dir:
return False, f"No se encontró el documento con ID {document_id}.", None
# Cargar metadatos del documento
meta_file = os.path.join(document_dir, 'meta.json')
meta_file = os.path.join(document_dir, "meta.json")
document_meta = load_json_file(meta_file)
if not document_meta:
return False, "Error al cargar metadatos del documento.", None
# Validar tipo de archivo
allowed_filetypes = get_allowed_filetypes()
filename = secure_filename(file.filename)
extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
extension = filename.rsplit(".", 1)[1].lower() if "." in filename else ""
if extension not in allowed_filetypes:
return False, f"Tipo de archivo no permitido: {extension}", None
# Verificar MIME type
if not check_file_type(file.stream, [allowed_filetypes[extension]['mime_type']]):
if not check_file_type(file.stream, [allowed_filetypes[extension]["mime_type"]]):
return False, "El tipo de archivo no coincide con su extensión.", None
# Determinar número de versión
last_version = max([v['version'] for v in document_meta['versions']])
last_version = max([v["version"] for v in document_meta["versions"]])
new_version = last_version + 1
# Preparar nombre de archivo
doc_name = document_meta['document_id'].split('_', 1)[1] if '_' in document_meta['document_id'] else 'document'
doc_name = (
document_meta["document_id"].split("_", 1)[1]
if "_" in document_meta["document_id"]
else "document"
)
version_filename = format_version_filename(new_version, doc_name, extension)
version_path = os.path.join(document_dir, version_filename)
# Guardar archivo
file.seek(0)
file.save(version_path)
# Calcular checksum
checksum = calculate_checksum(version_path)
# Obtener tamaño del archivo
file_size = os.path.getsize(version_path)
# Preparar metadatos de la versión
version_meta = {
'version': new_version,
'filename': version_filename,
'created_at': datetime.now(pytz.UTC).isoformat(),
'created_by': creator_username,
'description': version_data.get('description', f'Versión {new_version}'),
'file_size': file_size,
'mime_type': allowed_filetypes[extension]['mime_type'],
'checksum': checksum,
'downloads': []
"version": new_version,
"filename": version_filename,
"created_at": datetime.now(pytz.UTC).isoformat(),
"created_by": creator_username,
"description": version_data.get("description", f"Versión {new_version}"),
"file_size": file_size,
"mime_type": allowed_filetypes[extension]["mime_type"],
"checksum": checksum,
"downloads": [],
}
# Añadir versión a metadatos
document_meta['versions'].append(version_meta)
document_meta["versions"].append(version_meta)
# Guardar metadatos actualizados
save_json_file(meta_file, document_meta)
return True, "Nueva versión añadida correctamente.", new_version
def get_document(project_id, document_id):
"""
Obtener información de un documento.
Args:
project_id (int): ID del proyecto
document_id (int): ID del documento
Returns:
dict: Datos del documento o None si no existe
"""
# Buscar directorio del proyecto
project_dir = find_project_directory(project_id)
if not project_dir:
return None
# Buscar documento
document_dir = find_document_directory(project_dir, document_id)
if not document_dir:
return None
# Cargar metadatos
meta_file = os.path.join(document_dir, 'meta.json')
meta_file = os.path.join(document_dir, "meta.json")
document_meta = load_json_file(meta_file)
# Agregar ruta del directorio
if document_meta:
document_meta['directory'] = os.path.basename(document_dir)
document_meta["directory"] = os.path.basename(document_dir)
return document_meta
def get_document_version(project_id, document_id, version):
"""
Obtener información de una versión específica de un documento.
Args:
project_id (int): ID del proyecto
document_id (int): ID del documento
version (int): Número de versión
Returns:
tuple: (dict, str) - (Metadatos de la versión, ruta al archivo)
"""
document = get_document(project_id, document_id)
if not document:
return None, None
# Buscar versión específica
version_meta = None
for v in document['versions']:
if v['version'] == int(version):
for v in document["versions"]:
if v["version"] == int(version):
version_meta = v
break
if not version_meta:
return None, None
# Preparar ruta al archivo
project_dir = find_project_directory(project_id)
document_dir = find_document_directory(project_dir, document_id)
file_path = os.path.join(document_dir, version_meta['filename'])
file_path = os.path.join(document_dir, version_meta["filename"])
return version_meta, file_path
def get_latest_version(project_id, document_id):
"""
Obtener la última versión de un documento.
Args:
project_id (int): ID del proyecto
document_id (int): ID del documento
Returns:
tuple: (dict, str) - (Metadatos de la versión, ruta al archivo)
"""
document = get_document(project_id, document_id)
if not document or not document['versions']:
if not document or not document["versions"]:
return None, None
# Encontrar la versión más reciente
latest_version = max(document['versions'], key=lambda v: v['version'])
latest_version = max(document["versions"], key=lambda v: v["version"])
# Preparar ruta al archivo
project_dir = find_project_directory(project_id)
document_dir = find_document_directory(project_dir, document_id)
file_path = os.path.join(document_dir, latest_version['filename'])
file_path = os.path.join(document_dir, latest_version["filename"])
return latest_version, file_path
def register_download(project_id, document_id, version, username):
"""
Registrar una descarga de documento.
Args:
project_id (int): ID del proyecto
document_id (int): ID del documento
version (int): Número de versión
username (str): Usuario que descarga
Returns:
bool: True si se registró correctamente, False en caso contrario
"""
# Buscar documento
project_dir = find_project_directory(project_id)
if not project_dir:
return False
document_dir = find_document_directory(project_dir, document_id)
if not document_dir:
return False
# Cargar metadatos
meta_file = os.path.join(document_dir, 'meta.json')
meta_file = os.path.join(document_dir, "meta.json")
document_meta = load_json_file(meta_file)
if not document_meta:
return False
# Buscar versión
for v in document_meta['versions']:
if v['version'] == int(version):
for v in document_meta["versions"]:
if v["version"] == int(version):
# Registrar descarga
download_info = {
'user_id': username,
'downloaded_at': datetime.now(pytz.UTC).isoformat()
"user_id": username,
"downloaded_at": datetime.now(pytz.UTC).isoformat(),
}
v['downloads'].append(download_info)
v["downloads"].append(download_info)
# Guardar metadatos actualizados
save_json_file(meta_file, document_meta)
return True
return False
def find_document_directory(project_dir, document_id):
"""
Encontrar el directorio de un documento por su ID.
Args:
project_dir (str): Ruta al directorio del proyecto
document_id (int): ID del documento
Returns:
str: Ruta al directorio o None si no se encuentra
"""
documents_dir = os.path.join(project_dir, 'documents')
documents_dir = os.path.join(project_dir, "documents")
if not os.path.exists(documents_dir):
return None
# Prefijo a buscar en nombres de directorios
prefix = f"@{int(document_id):03d}_@"
for dir_name in os.listdir(documents_dir):
if dir_name.startswith(prefix):
return os.path.join(documents_dir, dir_name)
return None
def get_project_documents(project_id):
"""
Obtener todos los documentos de un proyecto.
Args:
project_id (int): ID del proyecto
Returns:
list: Lista de documentos
"""
project_dir = find_project_directory(project_id)
if not project_dir:
return []
documents_dir = os.path.join(project_dir, 'documents')
documents_dir = os.path.join(project_dir, "documents")
if not os.path.exists(documents_dir):
return []
documents = []
# Iterar sobre directorios de documentos
for dir_name in os.listdir(documents_dir):
if dir_name.startswith('@') and os.path.isdir(os.path.join(documents_dir, dir_name)):
if dir_name.startswith("@") and os.path.isdir(
os.path.join(documents_dir, dir_name)
):
document_dir = os.path.join(documents_dir, dir_name)
meta_file = os.path.join(document_dir, 'meta.json')
meta_file = os.path.join(document_dir, "meta.json")
if os.path.exists(meta_file):
document_meta = load_json_file(meta_file)
if document_meta:
# Extraer ID del documento del nombre del directorio
try:
doc_id = int(dir_name.split('_', 1)[0].replace('@', ''))
document_meta['id'] = doc_id
document_meta['directory'] = dir_name
doc_id = int(dir_name.split("_", 1)[0].replace("@", ""))
document_meta["id"] = doc_id
document_meta["directory"] = dir_name
documents.append(document_meta)
except (ValueError, IndexError):
pass
return documents
def delete_document(project_id, document_id):
"""
Eliminar un documento.
Args:
project_id (int): ID del proyecto
document_id (int): ID del documento
Returns:
tuple: (success, message)
"""
# Buscar documento
project_dir = find_project_directory(project_id)
if not project_dir:
return False, f"No se encontró el proyecto con ID {project_id}."
document_dir = find_document_directory(project_dir, document_id)
if not document_dir:
return False, f"No se encontró el documento con ID {document_id}."
# Eliminar directorio y contenido
try:
import shutil
shutil.rmtree(document_dir)
return True, "Documento eliminado correctamente."
except Exception as e:
return False, f"Error al eliminar el documento: {str(e)}"
return False, f"Error al eliminar el documento: {str(e)}"

View File

@ -4,273 +4,290 @@ from datetime import datetime
import pytz
from flask import current_app
from utils.file_utils import (
load_json_file, save_json_file, ensure_dir_exists,
get_next_id, format_project_directory_name
load_json_file,
save_json_file,
ensure_dir_exists,
get_next_id,
format_project_directory_name,
)
def create_project(project_data, creator_username):
"""
Crear un nuevo proyecto.
Args:
project_data (dict): Datos del proyecto
creator_username (str): Usuario que crea el proyecto
Returns:
tuple: (success, message, project_id)
"""
# Validar datos obligatorios
required_fields = ['descripcion', 'cliente', 'esquema']
required_fields = ["descripcion", "cliente", "esquema"]
for field in required_fields:
if field not in project_data or not project_data[field]:
return False, f"El campo '{field}' es obligatorio.", None
# Obtener siguiente ID de proyecto
project_id = get_next_id('project')
project_id = get_next_id("project")
# Crear código de proyecto (PROJ001, etc.)
project_code = f"PROJ{project_id:03d}"
# Preparar directorio del proyecto
storage_path = current_app.config['STORAGE_PATH']
dir_name = format_project_directory_name(project_id, project_data['descripcion'])
project_dir = os.path.join(storage_path, 'projects', dir_name)
storage_path = current_app.config["STORAGE_PATH"]
dir_name = format_project_directory_name(project_id, project_data["descripcion"])
project_dir = os.path.join(storage_path, "projects", dir_name)
# Verificar si ya existe
if os.path.exists(project_dir):
return False, "Ya existe un proyecto con esa descripción.", None
# Crear directorios
ensure_dir_exists(project_dir)
ensure_dir_exists(os.path.join(project_dir, 'documents'))
ensure_dir_exists(os.path.join(project_dir, "documents"))
# Crear metadatos del proyecto
project_meta = {
'codigo': project_code,
'proyecto_padre': project_data.get('proyecto_padre'),
'esquema': project_data['esquema'],
'descripcion': project_data['descripcion'],
'cliente': project_data['cliente'],
'destinacion': project_data.get('destinacion', ''),
'ano_creacion': datetime.now().year,
'fecha_creacion': datetime.now(pytz.UTC).isoformat(),
'creado_por': creator_username,
'estado': 'activo',
'ultima_modificacion': datetime.now(pytz.UTC).isoformat(),
'modificado_por': creator_username
"codigo": project_code,
"proyecto_padre": project_data.get("proyecto_padre"),
"esquema": project_data["esquema"],
"descripcion": project_data["descripcion"],
"cliente": project_data["cliente"],
"destinacion": project_data.get("destinacion", ""),
"ano_creacion": datetime.now().year,
"fecha_creacion": datetime.now(pytz.UTC).isoformat(),
"creado_por": creator_username,
"estado": "activo",
"ultima_modificacion": datetime.now(pytz.UTC).isoformat(),
"modificado_por": creator_username,
}
# Guardar metadatos
meta_file = os.path.join(project_dir, 'project_meta.json')
meta_file = os.path.join(project_dir, "project_meta.json")
save_json_file(meta_file, project_meta)
# Guardar permisos del proyecto (inicialmente vacío)
permissions_file = os.path.join(project_dir, 'permissions.json')
permissions_file = os.path.join(project_dir, "permissions.json")
save_json_file(permissions_file, {})
# Copiar el esquema seleccionado
schema_file = os.path.join(storage_path, 'schemas', 'schema.json')
schema_file = os.path.join(storage_path, "schemas", "schema.json")
schemas = load_json_file(schema_file, {})
if project_data['esquema'] in schemas:
project_schema_file = os.path.join(project_dir, 'schema.json')
save_json_file(project_schema_file, schemas[project_data['esquema']])
if project_data["esquema"] in schemas:
project_schema_file = os.path.join(project_dir, "schema.json")
save_json_file(project_schema_file, schemas[project_data["esquema"]])
return True, "Proyecto creado correctamente.", project_id
def update_project(project_id, project_data, modifier_username):
"""
Actualizar un proyecto existente.
Args:
project_id (int): ID del proyecto
project_data (dict): Datos actualizados
modifier_username (str): Usuario que modifica
Returns:
tuple: (success, message)
"""
# Buscar el proyecto
project_dir = find_project_directory(project_id)
if not project_dir:
return False, f"No se encontró el proyecto con ID {project_id}."
# Cargar metadatos actuales
meta_file = os.path.join(project_dir, 'project_meta.json')
meta_file = os.path.join(project_dir, "project_meta.json")
current_meta = load_json_file(meta_file)
# Actualizar campos
for key, value in project_data.items():
if key in current_meta and key not in ['codigo', 'fecha_creacion', 'creado_por', 'ano_creacion']:
if key in current_meta and key not in [
"codigo",
"fecha_creacion",
"creado_por",
"ano_creacion",
]:
current_meta[key] = value
# Actualizar metadatos de modificación
current_meta['ultima_modificacion'] = datetime.now(pytz.UTC).isoformat()
current_meta['modificado_por'] = modifier_username
current_meta["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat()
current_meta["modificado_por"] = modifier_username
# Guardar metadatos actualizados
save_json_file(meta_file, current_meta)
return True, "Proyecto actualizado correctamente."
def get_project(project_id):
"""
Obtener información de un proyecto.
Args:
project_id (int): ID del proyecto
Returns:
dict: Datos del proyecto o None si no existe
"""
project_dir = find_project_directory(project_id)
if not project_dir:
return None
# Cargar metadatos
meta_file = os.path.join(project_dir, 'project_meta.json')
meta_file = os.path.join(project_dir, "project_meta.json")
project_meta = load_json_file(meta_file)
# Agregar la ruta del directorio
project_meta['directory'] = os.path.basename(project_dir)
project_meta["directory"] = os.path.basename(project_dir)
return project_meta
def delete_project(project_id):
"""
Eliminar un proyecto (marcar como inactivo).
Args:
project_id (int): ID del proyecto
Returns:
tuple: (success, message)
"""
project_dir = find_project_directory(project_id)
if not project_dir:
return False, f"No se encontró el proyecto con ID {project_id}."
# Cargar metadatos
meta_file = os.path.join(project_dir, 'project_meta.json')
meta_file = os.path.join(project_dir, "project_meta.json")
project_meta = load_json_file(meta_file)
# Marcar como inactivo (no eliminar físicamente)
project_meta['estado'] = 'inactivo'
project_meta['ultima_modificacion'] = datetime.now(pytz.UTC).isoformat()
project_meta["estado"] = "inactivo"
project_meta["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat()
# Guardar metadatos actualizados
save_json_file(meta_file, project_meta)
return True, "Proyecto marcado como inactivo."
def get_all_projects(include_inactive=False):
"""
Obtener todos los proyectos.
Args:
include_inactive (bool): Incluir proyectos inactivos
Returns:
list: Lista de proyectos
"""
storage_path = current_app.config['STORAGE_PATH']
projects_dir = os.path.join(storage_path, 'projects')
storage_path = current_app.config["STORAGE_PATH"]
projects_dir = os.path.join(storage_path, "projects")
projects = []
# Iterar sobre directorios de proyectos
for dir_name in os.listdir(projects_dir):
if dir_name.startswith('@'): # Formato de directorio de proyecto
if dir_name.startswith("@"): # Formato de directorio de proyecto
project_dir = os.path.join(projects_dir, dir_name)
if os.path.isdir(project_dir):
meta_file = os.path.join(project_dir, 'project_meta.json')
meta_file = os.path.join(project_dir, "project_meta.json")
if os.path.exists(meta_file):
project_meta = load_json_file(meta_file)
# Incluir solo si está activo o se solicitan inactivos
if include_inactive or project_meta.get('estado') == 'activo':
if include_inactive or project_meta.get("estado") == "activo":
# Agregar la ruta del directorio
project_meta['directory'] = dir_name
project_meta["directory"] = dir_name
projects.append(project_meta)
return projects
def find_project_directory(project_id):
"""
Encontrar el directorio de un proyecto por su ID.
Args:
project_id (int): ID del proyecto
Returns:
str: Ruta al directorio o None si no se encuentra
"""
storage_path = current_app.config['STORAGE_PATH']
projects_dir = os.path.join(storage_path, 'projects')
storage_path = current_app.config["STORAGE_PATH"]
projects_dir = os.path.join(storage_path, "projects")
# Prefijo a buscar en nombres de directorios
prefix = f"@{int(project_id):03d}_@"
for dir_name in os.listdir(projects_dir):
if dir_name.startswith(prefix):
return os.path.join(projects_dir, dir_name)
return None
def get_project_children(project_id):
"""
Obtener proyectos hijos de un proyecto.
Args:
project_id (int): ID del proyecto padre
Returns:
list: Lista de proyectos hijos
"""
all_projects = get_all_projects()
project_code = f"PROJ{int(project_id):03d}"
# Filtrar proyectos con este padre
children = [p for p in all_projects if p.get('proyecto_padre') == project_code]
children = [p for p in all_projects if p.get("proyecto_padre") == project_code]
return children
def get_project_document_count(project_id):
"""
Contar documentos en un proyecto.
Args:
project_id (int): ID del proyecto
Returns:
int: Número de documentos
"""
project_dir = find_project_directory(project_id)
if not project_dir:
return 0
documents_dir = os.path.join(project_dir, 'documents')
documents_dir = os.path.join(project_dir, "documents")
if not os.path.exists(documents_dir):
return 0
# Contar directorios de documentos
count = 0
for item in os.listdir(documents_dir):
if os.path.isdir(os.path.join(documents_dir, item)) and item.startswith('@'):
if os.path.isdir(os.path.join(documents_dir, item)) and item.startswith("@"):
count += 1
return count
def filter_projects(filter_params):
"""
Filtrar proyectos según los parámetros proporcionados.
Args:
filter_params (dict): Diccionario con parámetros de filtrado
- cliente: Nombre de cliente
@ -278,41 +295,75 @@ def filter_projects(filter_params):
- ano_inicio: Año de inicio para filtrar
- ano_fin: Año final para filtrar
- descripcion: Término de búsqueda en descripción
Returns:
list: Lista de proyectos que cumplen los criterios
"""
# Obtener todos los proyectos (incluyendo inactivos si se solicitan)
include_inactive = filter_params.get('estado') == 'inactivo'
include_inactive = filter_params.get("estado") == "inactivo"
all_projects = get_all_projects(include_inactive)
filtered_projects = []
for project in all_projects:
# Filtrar por cliente
if 'cliente' in filter_params and filter_params['cliente']:
if project['cliente'] != filter_params['cliente']:
if "cliente" in filter_params and filter_params["cliente"]:
if project["cliente"] != filter_params["cliente"]:
continue
# Filtrar por estado
if 'estado' in filter_params and filter_params['estado']:
if project['estado'] != filter_params['estado']:
if "estado" in filter_params and filter_params["estado"]:
if project["estado"] != filter_params["estado"]:
continue
# Filtrar por año de creación (rango)
if 'ano_inicio' in filter_params and filter_params['ano_inicio']:
if project['ano_creacion'] < int(filter_params['ano_inicio']):
if "ano_inicio" in filter_params and filter_params["ano_inicio"]:
if project["ano_creacion"] < int(filter_params["ano_inicio"]):
continue
if 'ano_fin' in filter_params and filter_params['ano_fin']:
if project['ano_creacion'] > int(filter_params['ano_fin']):
if "ano_fin" in filter_params and filter_params["ano_fin"]:
if project["ano_creacion"] > int(filter_params["ano_fin"]):
continue
# Filtrar por término en descripción
if 'descripcion' in filter_params and filter_params['descripcion']:
if filter_params['descripcion'].lower() not in project['descripcion'].lower():
if "descripcion" in filter_params and filter_params["descripcion"]:
if (
filter_params["descripcion"].lower()
not in project["descripcion"].lower()
):
continue
# Si pasó todos los filtros, agregar a la lista
filtered_projects.append(project)
return filtered_projects
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
def get_schemas_file_path():
"""Obtener ruta al archivo de esquemas."""
def get_schema_file_path():
"""Get path to the schema storage file."""
storage_path = current_app.config["STORAGE_PATH"]
return os.path.join(storage_path, "schemas", "schema.json")
def load_schemas():
"""Load all schemas from storage."""
file_path = get_schema_file_path()
if os.path.exists(file_path):
with open(file_path, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def save_schemas(schemas):
"""Save schemas to storage."""
file_path = get_schema_file_path()
with open(file_path, "w", encoding="utf-8") as f:
json.dump(schemas, f, ensure_ascii=False, indent=2)
return True
def get_all_schemas():
"""Obtener todos los esquemas disponibles."""
return load_json_file(get_schemas_file_path(), {})
"""Get all schemas as a list."""
schemas_dict = load_schemas()
return [
{**schema_data, "id": schema_id}
for schema_id, schema_data in schemas_dict.items()
]
def get_schema_by_id(schema_id):
"""
Obtener un esquema por su ID.
Args:
schema_id (str): ID del esquema a buscar
Returns:
dict: Datos del esquema o None si no existe
"""
schemas = get_all_schemas()
return schemas.get(schema_id)
"""Get a specific schema by ID."""
schemas = load_schemas()
if schema_id in schemas:
return {**schemas[schema_id], "id": schema_id}
return None
def create_schema(schema_data, user_id):
@ -42,7 +57,7 @@ def create_schema(schema_data, user_id):
Returns:
tuple: (éxito, mensaje)
"""
schemas = get_all_schemas()
schemas = load_schemas()
# Verificar si ya existe un esquema con ese código
schema_id = schema_data["codigo"]
@ -55,38 +70,25 @@ def create_schema(schema_data, user_id):
# Guardar esquema
schemas[schema_id] = schema_data
save_json_file(get_schemas_file_path(), schemas)
save_schemas(schemas)
return True, f"Esquema '{schema_data['descripcion']}' creado correctamente."
def update_schema(schema_id, schema_data):
"""
Actualizar un esquema existente.
Args:
schema_id (str): ID del esquema a actualizar
schema_data (dict): Nuevos datos del esquema
Returns:
tuple: (éxito, mensaje)
"""
schemas = get_all_schemas()
# Verificar si existe el esquema
"""Update an existing schema."""
schemas = load_schemas()
if schema_id not in schemas:
return False, f"No existe un esquema con el código {schema_id}."
return False
# Preservar metadatos originales
schema_data["fecha_creacion"] = schemas[schema_id].get("fecha_creacion")
schema_data["creado_por"] = schemas[schema_id].get("creado_por")
schema_data["ultima_modificacion"] = datetime.now(pytz.UTC).isoformat()
# Update modification timestamp
schema_data["updated_at"] = datetime.now().isoformat()
# Actualizar esquema
schemas[schema_id] = schema_data
save_json_file(get_schemas_file_path(), schemas)
# Remove id from data before saving (it's used as the key)
data_to_save = {k: v for k, v in schema_data.items() if k != "id"}
return True, f"Esquema '{schema_data['descripcion']}' actualizado correctamente."
schemas[schema_id] = data_to_save
return save_schemas(schemas)
def delete_schema(schema_id):
@ -99,7 +101,7 @@ def delete_schema(schema_id):
Returns:
tuple: (éxito, mensaje)
"""
schemas = get_all_schemas()
schemas = load_schemas()
# Verificar si existe el esquema
if schema_id not in schemas:
@ -111,51 +113,42 @@ def delete_schema(schema_id):
# Eliminar esquema
schema_desc = schemas[schema_id].get("descripcion", schema_id)
del schemas[schema_id]
save_json_file(get_schemas_file_path(), schemas)
save_schemas(schemas)
return True, f"Esquema '{schema_desc}' eliminado correctamente."
def initialize_default_schemas():
"""Inicializar esquemas predeterminados si no existen."""
schemas = get_all_schemas()
# Si ya hay esquemas, no hacer nada
if schemas:
return
# Esquema predeterminado para proyecto estándar
default_schema = {
"ESQ001": {
"codigo": "ESQ001",
"descripcion": "Proyecto estándar",
"fecha_creacion": datetime.now(pytz.UTC).isoformat(),
"creado_por": "admin",
"documentos": [
{
"tipo": "pdf",
"nombre": "Manual de Usuario",
"nivel_ver": 0,
"nivel_editar": 5000,
},
{
"tipo": "dwg",
"nombre": "Planos Técnicos",
"nivel_ver": 0,
"nivel_editar": 5000,
},
{
"tipo": "zip",
"nombre": "Archivos Fuente",
"nivel_ver": 1000,
"nivel_editar": 5000,
},
],
"""Initialize default schemas if none exist."""
schemas = load_schemas()
if not schemas:
default_schemas = {
"default": {
"name": "Documento Estándar",
"description": "Esquema básico para documentos",
"fields": [
{"name": "title", "type": "text", "required": True},
{"name": "content", "type": "textarea", "required": True},
{"name": "tags", "type": "tags", "required": False},
],
"created_by": "system",
"created_at": datetime.now().isoformat(),
},
"report": {
"name": "Informe",
"description": "Esquema para informes formales",
"fields": [
{"name": "title", "type": "text", "required": True},
{"name": "summary", "type": "textarea", "required": True},
{"name": "body", "type": "richtext", "required": True},
{"name": "date", "type": "date", "required": True},
{"name": "author", "type": "text", "required": True},
],
"created_by": "system",
"created_at": datetime.now().isoformat(),
},
}
}
save_json_file(get_schemas_file_path(), default_schema)
current_app.logger.info("Esquemas predeterminados inicializados.")
save_schemas(default_schemas)
def get_schema_document_types(schema_id):

View File

@ -1,79 +1,59 @@
{% extends "base.html" %}
{% block title %}Crear Proyecto - ARCH{% endblock %}
{% block title %}Crear Nuevo Proyecto{% endblock %}
{% block content %}
<div class="container mt-4">
<h1>Crear Nuevo Proyecto</h1>
<h2>Crear Nuevo Proyecto</h2>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Información del Proyecto</h5>
<form method="POST">
{{ form.csrf_token }}
<!-- or use this alternative if not using WTForms: -->
<!-- <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> -->
<div class="mb-3">
<label for="descripcion" class="form-label">Descripción</label>
<input type="text" class="form-control" id="descripcion" name="descripcion" required>
</div>
<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">
<label for="descripcion" class="form-label">Descripción</label>
<textarea class="form-control" id="descripcion" name="descripcion" rows="3" required></textarea>
</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 class="mb-3">
<label for="cliente" class="form-label">Cliente</label>
<input type="text" class="form-control" id="cliente" name="cliente">
</div>
<div class="mb-3">
<label for="destinacion" class="form-label">Destinación</label>
<input type="text" class="form-control" id="destinacion" name="destinacion">
</div>
<div class="mb-3">
<label for="proyecto_padre" class="form-label">Proyecto Padre (opcional)</label>
<select class="form-select" id="proyecto_padre" name="proyecto_padre">
<option value="">Ninguno</option>
{% if projects %}
{% for project in projects %}
<option value="{{ project.id }}">{{ project.codigo }} - {{ project.descripcion }}</option>
{% endfor %}
{% endif %}
</select>
</div>
<div class="mb-3">
<label for="estado" class="form-label">Estado</label>
<select class="form-select" id="estado" name="estado">
<option value="activo" selected>Activo</option>
<option value="archivado">Archivado</option>
<option value="suspendido">Suspendido</option>
</select>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">Crear Proyecto</button>
<a href="{{ url_for('projects.list') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
<div class="mb-3">
<label for="cliente" class="form-label">Cliente</label>
<input type="text" class="form-control" id="cliente" name="cliente" required>
</div>
</div>
<div class="mb-3">
<label for="destinacion" class="form-label">Destinación</label>
<input type="text" class="form-control" id="destinacion" name="destinacion">
</div>
<div class="mb-3">
<label for="esquema" class="form-label">Esquema</label>
<select class="form-select" id="esquema" name="esquema" required>
<option value="">Seleccione un esquema</option>
{% if form.esquema.choices %}
{% for value, label in form.esquema.choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
{% endif %}
</select>
</div>
<div class="mb-3">
<label for="proyecto_padre" class="form-label">Proyecto Padre</label>
<select class="form-select" id="proyecto_padre" name="proyecto_padre">
<option value="">Ninguno</option>
{% if form.proyecto_padre.choices %}
{% for value, label in form.proyecto_padre.choices %}
{% if value %}
<option value="{{ value }}">{{ label }}</option>
{% endif %}
{% endfor %}
{% endif %}
</select>
</div>
<button type="submit" class="btn btn-primary">Crear Proyecto</button>
<a href="{{ url_for('projects.list') }}" class="btn btn-secondary">Cancelar</a>
</form>
</div>
{% 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" %}
{% block title %}{{ project.descripcion }} - ARCH{% endblock %}
{% block title %}Proyecto: {{ project.descripcion }}{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/projects.css') }}">
@ -10,175 +10,153 @@
{% block page_title %}Proyecto: {{ project.descripcion }}{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Detalles del Proyecto</h5>
<span class="badge bg-{{ 'success' if project.estado == 'activo' else 'danger' }}">
{{ project.estado|capitalize }}
</span>
</div>
<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 class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<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="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 %}
<div class="card mt-3">
<div class="card-header">
<h5 class="card-title mb-0">Subproyectos</h5>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for child in children %}
<a href="{{ url_for('projects.view', project_id=child.codigo|replace('PROJ', '')|int) }}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
{{ child.descripcion }}
<span class="badge bg-{{ 'success' if child.estado == 'activo' else 'danger' }} rounded-pill">
{{ child.estado|capitalize }}
</span>
</a>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Sección de documentos -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Documentos del Proyecto</h5>
<a href="{{ url_for('documents.list', project_id=project.codigo|replace('PROJ', '')|int) }}"
class="btn btn-sm btn-outline-primary">
Ver todos
</a>
<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="card-body">
<div id="documents-container">
<div class="text-center py-5">
<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">
<h4 class="mb-0">Proyectos Relacionados</h4>
<a href="{{ url_for('projects.create') }}" class="btn btn-outline-success" title="Nuevo proyecto relacionado">
<i class="fas fa-plus fa-lg"></i>
</a>
</div>
<div class="card-body">
{% if children %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Código</th>
<th>Descripción</th>
<th>Cliente</th>
<th>Estado</th>
<th width="100">Acciones</th>
</tr>
</thead>
<tbody>
{% for child in children %}
<tr>
<td>{{ child.codigo }}</td>
<td>{{ child.descripcion }}</td>
<td>{{ child.cliente }}</td>
<td><span class="badge {{ 'bg-success' if child.estado == 'activo' else 'bg-secondary' }}">{{ child.estado|capitalize }}</span></td>
<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 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 %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No hay proyectos relacionados.</p>
{% endif %}
</div>
</div>
<!-- Documentos -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0">Documentos ({{ document_count }})</h4>
<a href="{{ url_for('documents.upload', project_id=project.codigo) }}" class="btn btn-outline-success" title="Subir documento">
<i class="fas fa-upload fa-lg"></i>
</a>
</div>
<div class="card-body">
<div id="documents-container">
{% if document_count > 0 %}
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</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>
<!-- 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>
@ -191,7 +169,7 @@
// Cargar documentos del proyecto mediante AJAX
$(document).ready(function() {
$.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",
dataType: "json",
success: function(data) {
@ -251,10 +229,10 @@
'<td>' + latestVersion.created_by + '</td>' +
'<td>' +
'<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>' +
'</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>' +
'</a>' +
'</div>' +
@ -266,5 +244,15 @@
$("#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>
{% 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>
</thead>
<tbody>
{% for schema_id, schema in schemas.items() %}
{% for schema in schemas %}
<tr>
<td>{{ schema.codigo }}</td>
<td>{{ schema.id }}</td>
<td>{{ schema.descripcion }}</td>
<td>{{ schema.documentos|length }}</td>
<td>{{ schema.fecha_creacion|default('-') }}</td>
<td>
<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">
<i class="fas fa-eye"></i>
</a>
{% 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">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-outline-danger"
onclick="confirmDeleteSchema('{{ schema_id }}', '{{ schema.descripcion }}')"
onclick="confirmDeleteSchema('{{ schema.id }}', '{{ schema.descripcion }}')"
title="Eliminar">
<i class="fas fa-trash"></i>
</button>

View File

@ -1,21 +1,24 @@
{% extends "base.html" %}
{% block title %}{{ schema.codigo }} - Esquemas - ARCH{% endblock %}
{% block title %}{{ schema.nombre }} - Esquemas - ARCH{% endblock %}
{% block content %}
<div class="container my-5">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<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>
</nav>
<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) %}
<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
</a>
<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 @@
{
"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
}
]
}
}
{"SCHEMA1": {"name": "Test Schema", "descripcion": "Schema for testing"}, "SCHEMA2": {"name": "Another Schema", "descripcion": "Another schema for testing"}}

View File

@ -3,12 +3,12 @@
"nombre": "Administrador",
"username": "admin",
"email": "admin@example.com",
"password_hash": "$2b$04$UGmPF4BBNejhS7ZKA58/hu3A8DtXkx7/Fyw.8igRby6eDw/RwkvAq",
"password_hash": "$2b$04$SuOrsLdaXHIG85Ni/Jw9sOBbVfE/ImINWLhywDn4/sHZfTiFeXQX2",
"nivel": 9999,
"idioma": "es",
"fecha_caducidad": null,
"empresa": "",
"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