Segunda version - Agregado Nombre al eschema
This commit is contained in:
parent
9eb2bd5648
commit
f31e4eb886
22
app.py
22
app.py
|
@ -23,12 +23,24 @@ cache = Cache()
|
|||
|
||||
def create_app(config_name=None):
|
||||
"""Fábrica de aplicación Flask."""
|
||||
if config_name is None:
|
||||
config_name = os.environ.get("FLASK_ENV", "default")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config[config_name])
|
||||
config[config_name].init_app(app)
|
||||
app.config["ENV"] = config_name # Añade esta línea si no existe
|
||||
|
||||
# Debugging para ver qué está pasando
|
||||
print(f"Configurando aplicación en modo: {config_name}")
|
||||
print(f"ENV actual: {app.config['ENV']}")
|
||||
|
||||
if isinstance(config_name, dict):
|
||||
app.config.update(config_name)
|
||||
else:
|
||||
if config_name is None:
|
||||
config_name = os.environ.get("FLASK_ENV", "default")
|
||||
app.config.from_object(config[config_name])
|
||||
config[config_name].init_app(app)
|
||||
|
||||
# Asegurar que el SECRET_KEY está configurado
|
||||
if not app.config.get("SECRET_KEY"):
|
||||
app.config["SECRET_KEY"] = "default_secret_key_please_change_in_production"
|
||||
|
||||
# Inicializar extensiones
|
||||
login_manager.init_app(app)
|
||||
|
|
128
descripcion.md
128
descripcion.md
|
@ -103,7 +103,8 @@ La aplicación sigue una arquitectura basada en archivos JSON y sistema de fiche
|
|||
{
|
||||
"ESQ001": {
|
||||
"codigo": "ESQ001",
|
||||
"descripcion": "Proyecto estándar",
|
||||
"nombre": "Siemens",
|
||||
"descripcion": "Proyecto estándar Siemens",
|
||||
"fecha_creacion": "2023-05-10T10:00:00Z",
|
||||
"creado_por": "admin",
|
||||
"documentos": [
|
||||
|
@ -553,130 +554,6 @@ Se implementará un plugin personalizado para pytest que generará reportes en f
|
|||
- Cobertura de código
|
||||
- Detalles de errores encontrados
|
||||
|
||||
### 9.5 Ejemplo de Configuración de Pytest
|
||||
|
||||
```python
|
||||
# conftest.py
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
from app import create_app
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""Crear una instancia de la aplicación para pruebas."""
|
||||
# Configuración de prueba
|
||||
test_config = {
|
||||
'TESTING': True,
|
||||
'STORAGE_PATH': 'test_storage',
|
||||
'SECRET_KEY': 'test_key'
|
||||
}
|
||||
|
||||
# Crear directorio de almacenamiento para pruebas
|
||||
if not os.path.exists('test_storage'):
|
||||
os.makedirs('test_storage')
|
||||
|
||||
# Crear estructura básica
|
||||
for dir in ['users', 'schemas', 'filetypes', 'projects', 'logs']:
|
||||
if not os.path.exists(f'test_storage/{dir}'):
|
||||
os.makedirs(f'test_storage/{dir}')
|
||||
|
||||
# Crear app con configuración de prueba
|
||||
app = create_app(test_config)
|
||||
|
||||
yield app
|
||||
|
||||
# Limpiar después de las pruebas
|
||||
shutil.rmtree('test_storage')
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
"""Cliente de prueba para la aplicación."""
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture
|
||||
def auth(client):
|
||||
"""Helper para pruebas de autenticación."""
|
||||
class AuthActions:
|
||||
def login(self, username='admin', password='password'):
|
||||
return client.post('/login', data={
|
||||
'username': username,
|
||||
'password': password
|
||||
}, follow_redirects=True)
|
||||
|
||||
def logout(self):
|
||||
return client.get('/logout', follow_redirects=True)
|
||||
|
||||
return AuthActions()
|
||||
```
|
||||
|
||||
```python
|
||||
# json_reporter.py
|
||||
|
||||
import json
|
||||
import pytest
|
||||
import datetime
|
||||
import os
|
||||
|
||||
class JSONReporter:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.results = {
|
||||
'summary': {
|
||||
'total': 0,
|
||||
'passed': 0,
|
||||
'failed': 0,
|
||||
'skipped': 0,
|
||||
'duration': 0,
|
||||
'timestamp': datetime.datetime.now().isoformat()
|
||||
},
|
||||
'tests': []
|
||||
}
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.when == 'call' or (report.when == 'setup' and report.skipped):
|
||||
self.results['summary']['total'] += 1
|
||||
|
||||
if report.passed:
|
||||
result = 'passed'
|
||||
self.results['summary']['passed'] += 1
|
||||
elif report.failed:
|
||||
result = 'failed'
|
||||
self.results['summary']['failed'] += 1
|
||||
else:
|
||||
result = 'skipped'
|
||||
self.results['summary']['skipped'] += 1
|
||||
|
||||
self.results['tests'].append({
|
||||
'name': report.nodeid,
|
||||
'result': result,
|
||||
'duration': report.duration,
|
||||
'error': str(report.longrepr) if hasattr(report, 'longrepr') and report.longrepr else None
|
||||
})
|
||||
|
||||
def pytest_sessionfinish(self, session):
|
||||
self.results['summary']['duration'] = session.config.hook.pytest_report_teststatus.get_duration()
|
||||
|
||||
with open('test_results.json', 'w') as f:
|
||||
json.dump(self.results, f, indent=2)
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_configure(config):
|
||||
config.pluginmanager.register(JSONReporter(config), 'json_reporter')
|
||||
```
|
||||
|
||||
## 10. Guía de Implementación
|
||||
|
||||
### 10.1 Configuración del Entorno
|
||||
|
||||
1. Crear entorno virtual:
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
2. Instalar dependencias:
|
||||
```bash
|
||||
|
@ -747,6 +624,7 @@ filetypes = {
|
|||
schemas = {
|
||||
"ESQ001": {
|
||||
"codigo": "ESQ001",
|
||||
"nombre": "Siemens",
|
||||
"descripcion": "Proyecto estándar",
|
||||
"fecha_creacion": "2023-05-10T10:00:00Z",
|
||||
"creado_por": "admin",
|
||||
|
|
|
@ -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())
|
|
@ -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
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
83
run_tests.py
83
run_tests.py
|
@ -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))
|
|
@ -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)}"
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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 }}')">
|
||||
|
|
|
@ -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()
|
|
@ -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
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"max_project_id": 0,
|
||||
"max_document_id": 0
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Test Schema",
|
||||
"descripcion": "Schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Another Schema",
|
||||
"descripcion": "Another schema for testing"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Another Schema",
|
||||
"descripcion": "Another schema for testing"
|
||||
}
|
|
@ -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"}}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
# This file makes the tests directory a Python package
|
||||
# allowing the json_reporter module to be imported
|
|
@ -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)
|
|
@ -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
|
|
@ -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")
|
|
@ -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
|
|
@ -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()
|
||||
)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue