diff --git a/app/blueprints/jobs/__init__.py b/app/blueprints/jobs/__init__.py index 1f311bd2..679a6fe4 100644 --- a/app/blueprints/jobs/__init__.py +++ b/app/blueprints/jobs/__init__.py @@ -1,18 +1,7 @@ from flask import Blueprint -from flask_login import login_required bp = Blueprint('jobs', __name__) -@bp.before_request -@login_required -def before_request(): - ''' - Ensures that the routes in this package can only be visited by users that - are logged in. - ''' - pass - - -from . import routes +from . import routes, inputs, results diff --git a/app/blueprints/jobs/inputs/__init__.py b/app/blueprints/jobs/inputs/__init__.py new file mode 100644 index 00000000..e91268cf --- /dev/null +++ b/app/blueprints/jobs/inputs/__init__.py @@ -0,0 +1,2 @@ +from .. import bp +from . import routes diff --git a/app/blueprints/jobs/inputs/routes.py b/app/blueprints/jobs/inputs/routes.py new file mode 100644 index 00000000..f0ce4344 --- /dev/null +++ b/app/blueprints/jobs/inputs/routes.py @@ -0,0 +1,27 @@ +from flask import abort, send_from_directory +from flask_login import current_user, login_required +from app.models import JobInput +from . import bp + + +@bp.route('//inputs//download') +@login_required +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, + as_attachment=True, + download_name=job_input.filename, + mimetype=job_input.mimetype + ) diff --git a/app/blueprints/jobs/results/__init__.py b/app/blueprints/jobs/results/__init__.py new file mode 100644 index 00000000..e91268cf --- /dev/null +++ b/app/blueprints/jobs/results/__init__.py @@ -0,0 +1,2 @@ +from .. import bp +from . import routes diff --git a/app/blueprints/jobs/results/routes.py b/app/blueprints/jobs/results/routes.py new file mode 100644 index 00000000..9b24e598 --- /dev/null +++ b/app/blueprints/jobs/results/routes.py @@ -0,0 +1,27 @@ +from flask import abort, send_from_directory +from flask_login import current_user, login_required +from app.models import JobResult +from . import bp + + +@bp.route('//results//download') +@login_required +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, + as_attachment=True, + download_name=job_result.filename, + mimetype=job_result.mimetype + ) diff --git a/app/blueprints/jobs/routes.py b/app/blueprints/jobs/routes.py index 77e661cf..6691bdbc 100644 --- a/app/blueprints/jobs/routes.py +++ b/app/blueprints/jobs/routes.py @@ -5,36 +5,24 @@ from flask import ( jsonify, redirect, render_template, - send_from_directory, url_for ) -from flask_login import current_user +from flask_login import current_user, login_required from threading import Thread from app import db -from app.models import Job, JobInput, JobResult, JobStatus +from app.decorators import admin_required +from app.models import Job, 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('') +@login_required def jobs(): return redirect(url_for('main.dashboard', _anchor='jobs')) @bp.route('/') +@login_required def job(job_id: int): job = Job.query.get_or_404(job_id) @@ -51,7 +39,15 @@ def job(job_id: int): ) +def _delete_job(app: Flask, job_id: int): + with app.app_context(): + job = Job.query.get(job_id) + job.delete() + db.session.commit() + + @bp.route('/', methods=['DELETE']) +@login_required def delete_job(job_id: int): job = Job.query.get_or_404(job_id) @@ -71,12 +67,10 @@ def delete_job(job_id: int): @bp.route('//log') -def get_job_log(job_id: int): +@admin_required +def 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) @@ -87,7 +81,15 @@ def get_job_log(job_id: int): return jsonify(log) -@bp.route('//restart') +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('//restart', methods=['POST']) +@login_required def restart_job(job_id: int): job = Job.query.get_or_404(job_id) @@ -107,47 +109,3 @@ def restart_job(job_id: int): thread.start() return jsonify(f'Job "{job.title}" marked for restarting.'), 202 - - -@bp.route('//inputs//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, - as_attachment=True, - download_name=job_input.filename, - mimetype=job_input.mimetype - ) - - -@bp.route('//results//download') -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, - as_attachment=True, - download_name=job_result.filename, - mimetype=job_result.mimetype - ) diff --git a/app/models/job_input.py b/app/models/job_input.py index 8405a92b..9c3f187d 100644 --- a/app/models/job_input.py +++ b/app/models/job_input.py @@ -20,14 +20,6 @@ class JobInput(FileMixin, HashidMixin, db.Model): def __repr__(self): return f'' - @property - def content_url(self): - return url_for( - 'jobs.download_job_input', - job_id=self.job.id, - job_input_id=self.id - ) - @property def jsonpatch_path(self): return f'{self.job.jsonpatch_path}/inputs/{self.hashid}' @@ -40,7 +32,7 @@ class JobInput(FileMixin, HashidMixin, db.Model): def url(self): return url_for( 'jobs.job', - job_id=self.job_id, + job_input_id=self.id, _anchor=f'job-{self.job.hashid}-input-{self.hashid}' ) diff --git a/app/models/job_result.py b/app/models/job_result.py index b0c9c1e3..f4e141d5 100644 --- a/app/models/job_result.py +++ b/app/models/job_result.py @@ -22,14 +22,6 @@ class JobResult(FileMixin, HashidMixin, db.Model): def __repr__(self): return f'' - @property - def download_url(self): - return url_for( - 'jobs.download_job_result', - job_id=self.job_id, - job_result_id=self.id - ) - @property def jsonpatch_path(self): return f'{self.job.jsonpatch_path}/results/{self.hashid}' @@ -41,8 +33,8 @@ class JobResult(FileMixin, HashidMixin, db.Model): @property def url(self): return url_for( - 'jobs.job', - job_id=self.job_id, + 'job_results.job_result', + job_result_id=self.id, _anchor=f'job-{self.job.hashid}-result-{self.hashid}' ) diff --git a/app/static/js/app/endpoints/jobs.js b/app/static/js/app/endpoints/jobs.js index 9b6828f0..78aee075 100644 --- a/app/static/js/app/endpoints/jobs.js +++ b/app/static/js/app/endpoints/jobs.js @@ -1,4 +1,8 @@ nopaque.app.endpoints.Jobs = class Jobs { + constructor(app) { + this.app = app; + } + async delete(jobId) { const options = { headers: { @@ -34,7 +38,8 @@ nopaque.app.endpoints.Jobs = class Jobs { const options = { headers: { Accept: 'application/json' - } + }, + method: 'POST' }; const response = await fetch(`/jobs/${jobId}/restart`, options);