mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-01-27 01:50:34 +00:00
Move jobs namespace back to http routes
This commit is contained in:
parent
328f85ba52
commit
bb60a2ba67
@ -136,9 +136,6 @@ def create_app(config: Config = Config) -> Flask:
|
||||
from .namespaces.corpora import CorporaNamespace
|
||||
socketio.on_namespace(CorporaNamespace('/corpora'))
|
||||
|
||||
from .namespaces.jobs import JobsNamespace
|
||||
socketio.on_namespace(JobsNamespace('/jobs'))
|
||||
|
||||
from .namespaces.users import UsersNamespace
|
||||
socketio.on_namespace(UsersNamespace('/users'))
|
||||
# endregion SocketIO Namespaces
|
||||
|
@ -4,11 +4,17 @@ from . import bp
|
||||
|
||||
|
||||
@bp.app_errorhandler(HTTPException)
|
||||
def handle_http_exception(error):
|
||||
def handle_http_exception(e: HTTPException):
|
||||
''' Generic HTTP exception handler '''
|
||||
accept_json = request.accept_mimetypes.accept_json
|
||||
accept_html = request.accept_mimetypes.accept_html
|
||||
|
||||
if accept_json and not accept_html:
|
||||
response = jsonify(str(error))
|
||||
return response, error.code
|
||||
return render_template('errors/error.html.j2', error=error), error.code
|
||||
error = {
|
||||
'code': e.code,
|
||||
'name': e.name,
|
||||
'description': e.description
|
||||
}
|
||||
return jsonify(error), e.code
|
||||
|
||||
return render_template('errors/error.html.j2', error=e), e.code
|
||||
|
@ -1,25 +1,49 @@
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
Flask,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
send_from_directory,
|
||||
url_for
|
||||
)
|
||||
from flask_login import current_user
|
||||
from app.models import Job, JobInput, JobResult
|
||||
from threading import Thread
|
||||
from app import db
|
||||
from app.models import Job, JobInput, JobResult, JobStatus
|
||||
from . import bp
|
||||
|
||||
|
||||
def _delete_job(app: Flask, job_id: int):
|
||||
with app.app_context():
|
||||
job = Job.query.get(job_id)
|
||||
job.delete()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def _restart_job(app: Flask, job_id: int):
|
||||
with app.app_context():
|
||||
job = Job.query.get(job_id)
|
||||
job.restart()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@bp.route('')
|
||||
def jobs():
|
||||
return redirect(url_for('main.dashboard', _anchor='jobs'))
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>')
|
||||
def job(job_id):
|
||||
def job(job_id: int):
|
||||
job = Job.query.get_or_404(job_id)
|
||||
if not (job.user == current_user or current_user.is_administrator):
|
||||
|
||||
if not (
|
||||
job.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
return render_template(
|
||||
'jobs/job.html.j2',
|
||||
title='Job',
|
||||
@ -27,11 +51,77 @@ def job(job_id):
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
||||
def download_job_input(job_id, job_input_id):
|
||||
job_input = JobInput.query.filter_by(job_id=job_id, id=job_input_id).first_or_404()
|
||||
if not (job_input.job.user == current_user or current_user.is_administrator):
|
||||
@bp.route('/<hashid:job_id>', methods=['DELETE'])
|
||||
def delete_job(job_id: int):
|
||||
job = Job.query.get_or_404(job_id)
|
||||
|
||||
if not (
|
||||
job.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
thread = Thread(
|
||||
target=_delete_job,
|
||||
args=(current_app._get_current_object(), job.id)
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return jsonify(f'Job "{job.title}" marked for deletion.'), 202
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/log')
|
||||
def get_job_log(job_id: int):
|
||||
job = Job.query.get_or_404(job_id)
|
||||
|
||||
if not current_user.is_administrator:
|
||||
abort(403)
|
||||
|
||||
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
||||
abort(409)
|
||||
|
||||
log_file_path = job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt'
|
||||
with log_file_path.open() as log_file:
|
||||
log = log_file.read()
|
||||
|
||||
return jsonify(log)
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/restart')
|
||||
def restart_job(job_id: int):
|
||||
job = Job.query.get_or_404(job_id)
|
||||
|
||||
if not (
|
||||
job.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
if job.status != JobStatus.FAILED:
|
||||
abort(409)
|
||||
|
||||
thread = Thread(
|
||||
target=_restart_job,
|
||||
args=(current_app._get_current_object(), job.id)
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return jsonify(f'Job "{job.title}" marked for restarting.'), 202
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
||||
def download_job_input(job_id: int, job_input_id: int):
|
||||
job_input = JobInput.query.filter_by(
|
||||
job_id=job_id,
|
||||
id=job_input_id
|
||||
).first_or_404()
|
||||
|
||||
if not (
|
||||
job_input.job.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
return send_from_directory(
|
||||
job_input.path.parent,
|
||||
job_input.path.name,
|
||||
@ -42,10 +132,18 @@ def download_job_input(job_id, job_input_id):
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
|
||||
def download_job_result(job_id, job_result_id):
|
||||
job_result = JobResult.query.filter_by(job_id=job_id, id=job_result_id).first_or_404()
|
||||
if not (job_result.job.user == current_user or current_user.is_administrator):
|
||||
def download_job_result(job_id: int, job_result_id: int):
|
||||
job_result = JobResult.query.filter_by(
|
||||
job_id=job_id,
|
||||
id=job_result_id
|
||||
).first_or_404()
|
||||
|
||||
if not (
|
||||
job_result.job.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
return send_from_directory(
|
||||
job_result.path.parent,
|
||||
job_result.path.name,
|
||||
|
@ -1,118 +0,0 @@
|
||||
from flask import current_app, Flask
|
||||
from flask_login import current_user
|
||||
from flask_socketio import Namespace
|
||||
from app import db, hashids, socketio
|
||||
from app.decorators import socketio_admin_required, socketio_login_required
|
||||
from app.models import Job, JobStatus
|
||||
|
||||
|
||||
def _delete_job(app: Flask, job_id: int):
|
||||
with app.app_context():
|
||||
job = Job.query.get(job_id)
|
||||
job.delete()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def _restart_job(app: Flask, job_id: int):
|
||||
with app.app_context():
|
||||
job = Job.query.get(job_id)
|
||||
job.restart()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class JobsNamespace(Namespace):
|
||||
@socketio_login_required
|
||||
def on_delete(self, job_hashid: str) -> dict:
|
||||
if not isinstance(job_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
job_id = hashids.decode(job_hashid)
|
||||
|
||||
if not isinstance(job_id, int):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
job = Job.query.get(job_id)
|
||||
|
||||
if job is None:
|
||||
return {'status': 404, 'statusText': 'Not Found'}
|
||||
|
||||
if not (
|
||||
job.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
return {'status': 403, 'statusText': 'Forbidden'}
|
||||
|
||||
socketio.start_background_task(
|
||||
_delete_job,
|
||||
current_app._get_current_object(),
|
||||
job_id
|
||||
)
|
||||
|
||||
return {
|
||||
'body': f'Job "{job.title}" marked for deletion',
|
||||
'status': 202,
|
||||
'statusText': 'Accepted'
|
||||
}
|
||||
|
||||
@socketio_admin_required
|
||||
def on_log(self, job_hashid: str) -> dict:
|
||||
if not isinstance(job_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
job_id = hashids.decode(job_hashid)
|
||||
|
||||
if not isinstance(job_id, int):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
job = Job.query.get(job_id)
|
||||
|
||||
if job is None:
|
||||
return {'status': 404, 'statusText': 'Not Found'}
|
||||
|
||||
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
||||
return {'status': 409, 'statusText': 'Conflict'}
|
||||
|
||||
with open(job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt') as log_file:
|
||||
log = log_file.read()
|
||||
|
||||
return {
|
||||
'body': log,
|
||||
'status': 200,
|
||||
'statusText': 'OK'
|
||||
}
|
||||
|
||||
socketio_login_required
|
||||
def on_restart(self, job_hashid: str) -> dict:
|
||||
if not isinstance(job_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
job_id = hashids.decode(job_hashid)
|
||||
|
||||
if not isinstance(job_id, int):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
job = Job.query.get(job_id)
|
||||
|
||||
if job is None:
|
||||
return {'status': 404, 'statusText': 'Not Found'}
|
||||
|
||||
if not (
|
||||
job.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
return {'status': 403, 'statusText': 'Forbidden'}
|
||||
|
||||
if job.status == JobStatus.FAILED:
|
||||
return {'status': 409, 'statusText': 'Conflict'}
|
||||
|
||||
socketio.start_background_task(
|
||||
_restart_job,
|
||||
current_app._get_current_object(),
|
||||
job_id
|
||||
)
|
||||
|
||||
return {
|
||||
'body': f'Job "{job.title}" marked for restarting',
|
||||
'status': 202,
|
||||
'statusText': 'Accepted'
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
nopaque.App = class App {
|
||||
nopaque.app.Client = class Client {
|
||||
constructor() {
|
||||
this.socket = io({transports: ['websocket'], upgrade: false});
|
||||
|
@ -1,37 +1,47 @@
|
||||
nopaque.app.endpoints.Jobs = class Jobs {
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
async delete(jobId) {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
method: 'DELETE'
|
||||
};
|
||||
|
||||
this.socket = io('/jobs', {transports: ['websocket'], upgrade: false});
|
||||
const response = await fetch(`/jobs/${jobId}`, options);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
const response = await this.socket.emitWithAck('delete', id);
|
||||
async log(jobId) {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (response.status != 202) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(`/jobs/${jobId}/log`, options);
|
||||
const data = await response.json();
|
||||
|
||||
return response.body;
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async log(id) {
|
||||
const response = await this.socket.emitWithAck('log', id);
|
||||
async restart(jobId) {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(`/jobs/${jobId}/restart`, options);
|
||||
const data = await response.json();
|
||||
|
||||
return response.body;
|
||||
}
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
async restart(id) {
|
||||
const response = await this.socket.emitWithAck('restart', id);
|
||||
|
||||
if (response.status != 202) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,8 @@
|
||||
filters='rjsmin',
|
||||
output='gen/nopaque.%(version)s.js',
|
||||
'js/index.js',
|
||||
'js/app.js',
|
||||
'js/app/index.js',
|
||||
'js/app/client.js',
|
||||
'js/app/endpoints/index.js',
|
||||
'js/app/endpoints/corpora.js',
|
||||
'js/app/endpoints/jobs.js',
|
||||
@ -80,9 +80,9 @@
|
||||
<script src="{{ ASSET_URL }}"></script>
|
||||
{% endassets -%}
|
||||
|
||||
{# TODO: Think about implementing the following inside a main.js(.j2) #}
|
||||
<script>
|
||||
// TODO: Implement an app.run method and use this for all of the following
|
||||
const app = new nopaque.App();
|
||||
const app = new nopaque.app.Client();
|
||||
app.init();
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
|
@ -151,9 +151,9 @@
|
||||
|
||||
{% if current_user.is_administrator %}
|
||||
const jobLogButtonElement = document.querySelector('#job-log-button');
|
||||
const jobLogModalElement = document.querySelector('#job-log-modal');
|
||||
|
||||
jobLogButtonElement.addEventListener('click', async (event) => {
|
||||
const jobLogModalElement = document.querySelector('#job-log-modal');
|
||||
const log = await app.jobs.log({{ job.hashid|tojson }});
|
||||
jobLogModalElement.querySelector('pre code').textContent = log;
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user