diff --git a/app/main/views.py b/app/main/views.py index c238288183a75c55ec31d6373b3fc3e51d233d60..9a573a9f427ce7b2f105c43b85362e3fc79d52c9 100644 --- a/app/main/views.py +++ b/app/main/views.py @@ -5,7 +5,7 @@ from flask_login import current_user, login_required from . import main from .forms import CreateCorpusForm, QueryForm from .. import db -from ..models import Corpus, CorpusFile, Job +from ..models import Corpus, CorpusFile, Job, JobInput, JobResult from werkzeug.utils import secure_filename import os import threading @@ -20,47 +20,30 @@ def index(): @main.route('/corpora/<int:corpus_id>') @login_required def corpus(corpus_id): - if (current_user.is_administrator()): - corpus = Corpus.query.get_or_404(corpus_id) - else: - corpus = current_user.corpora.filter_by(id=corpus_id).first() - if not corpus: - print('Corpus not found.') - abort(404) - - dir = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'], - str(corpus.user_id), - 'corpora', - str(corpus.id)) - files = {} - for file in sorted(os.listdir(dir)): - files[file] = {} - files[file]['path'] = os.path.join(file) - + corpus = Corpus.query.get_or_404(corpus_id) + if not (corpus.creator == current_user + or current_user.is_administrator()): + abort(403) return render_template('main/corpora/corpus.html.j2', corpus=corpus, - files=files, - title='Corpus: ' + corpus.title) + title='Corpus') @main.route('/corpora/<int:corpus_id>/download') @login_required def corpus_download(corpus_id): - file = request.args.get('file') - if (current_user.is_administrator()): - corpus = Corpus.query.get_or_404(corpus_id) - else: - corpus = current_user.corpora.filter_by(id=corpus_id).first() - if not file or not corpus: - print('File not found.') + corpus_file_id = request.args.get('corpus_file_id') + corpus_file = CorpusFile.query.get_or_404(corpus_file_id) + if not corpus_file.corpus_id == corpus_id: abort(404) + if not (corpus_file.corpus.creator == current_user + or current_user.is_administrator()): + abort(403) dir = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'], - str(corpus.user_id), - 'corpora', - str(corpus.id)) + corpus_file.dir) return send_from_directory(as_attachment=True, directory=dir, - filename=file) + filename=corpus_file.filename) @main.route('/corpora/<int:corpus_id>/analysis', methods=['GET', 'POST']) @@ -83,20 +66,16 @@ def corpus_analysis(corpus_id): @login_required def dashboard(): create_corpus_form = CreateCorpusForm() - if create_corpus_form.validate_on_submit(): - app = current_app._get_current_object() corpus = Corpus(creator=current_user._get_current_object(), description=create_corpus_form.description.data, title=create_corpus_form.title.data) db.session.add(corpus) db.session.commit() - - dir = os.path.join(app.config['OPAQUE_STORAGE_DIRECTORY'], + dir = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'], str(corpus.user_id), 'corpora', str(corpus.id)) - try: os.makedirs(dir) except OSError: @@ -115,7 +94,6 @@ def dashboard(): db.session.commit() flash('Corpus created!') return redirect(url_for('main.dashboard')) - return render_template('main/dashboard.html.j2', create_corpus_form=create_corpus_form, title='Dashboard') @@ -124,58 +102,33 @@ def dashboard(): @main.route('/jobs/<int:job_id>') @login_required def job(job_id): - if (current_user.is_administrator()): - job = Job.query.get_or_404(job_id) - else: - job = current_user.jobs.filter_by(id=job_id).first() - if not job: - print('Job not found.') - abort(404) - - dir = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'], - str(job.user_id), - 'jobs', - str(job.id)) - files = {} - for file in sorted(os.listdir(dir)): - if file == 'output': - continue - files[file] = {} - files[file]['path'] = os.path.join(file) - if job.status == 'complete': - files[file]['results'] = {} - results_dir = os.path.join(dir, 'output', file) - for result in sorted(os.listdir(results_dir)): - result_type = result.rsplit(".", 1)[1] - files[file]['results'][result_type] = {} - files[file]['results'][result_type]['path'] = os.path.join( - 'output', files[file]['path'], result - ) - - return render_template('main/jobs/job.html.j2', - files=files, - job=job, - title='Job') + job = Job.query.get_or_404(job_id) + if not (job.creator == current_user or current_user.is_administrator()): + abort(403) + return render_template('main/jobs/job.html.j2', job=job, title='Job') @main.route('/jobs/<int:job_id>/download') @login_required def job_download(job_id): - file = request.args.get('file') - if (current_user.is_administrator()): - job = Job.query.get_or_404(job_id) + ressource_id = request.args.get('ressource_id') + ressource_type = request.args.get('ressource_type') + if ressource_type == 'input': + ressource = JobInput.query.get_or_404(ressource_id) + elif ressource_type == 'result': + ressource = JobResult.query.get_or_404(ressource_id) else: - job = current_user.jobs.filter_by(id=job_id).first() - if not file or not job: - print('File not found.') + abort(400) + if not ressource.job_id == job_id: abort(404) + if not (ressource.job.creator == current_user + or current_user.is_administrator()): + abort(403) dir = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'], - str(job.user_id), - 'jobs', - str(job.id)) + ressource.dir) return send_from_directory(as_attachment=True, directory=dir, - filename=file) + filename=ressource.filename) @main.route('/jobs/<int:job_id>/delete') diff --git a/app/templates/main/corpora/corpus.html.j2 b/app/templates/main/corpora/corpus.html.j2 index 321f9c284e0739ceb0e9daa9b598d2d176733fa4..1e47854c755b12f595c316a421142a10a7bd8428 100644 --- a/app/templates/main/corpora/corpus.html.j2 +++ b/app/templates/main/corpora/corpus.html.j2 @@ -1,85 +1,9 @@ {% extends "limited_width.html.j2" %} {% block page_content %} -<script> - var corpus_user_id = {{ corpus.user_id|tojson|safe }} - socket.emit('inspect_user', {{ corpus_user_id }}); -</script> -<script> - var CORPUS_ID = {{ corpus.id|tojson|safe }} - var foreignCorpusFlag; - {% if current_user.id == corpus.user_id %} - foreignCorpusFlag = false; - {% else %} - foreignCorpusFlag = true; - {% endif %} - - class InformationUpdater { - constructor(corpusId) { - this.corpusId = corpusId; - if (foreignCorpusFlag) { - foreignCorpusSubscribers.push(this); - } else { - corporaSubscribers.push(this); - } - } - - _init() { - var creationDateElement, descriptionElement, titleElement; - - if (foreignCorpusFlag) { - this.corpus = foreignCorpora[this.corpusId]; - } else { - this.corpus = corpora[this.corpusId]; - } - creationDateElement = document.getElementById("creation-date"); - creationDateElement.value = (new Date(this.corpus.creation_date * 1000)).toLocaleString(); - descriptionElement = document.getElementById("description"); - descriptionElement.innerHTML = this.corpus.description; - titleElement = document.getElementById("title"); - titleElement.innerHTML = this.corpus.title; - - M.updateTextFields(); - } - - _update(patch) { - var newStatusColor, operation, pathArray, status, statusColor, - updatedElement; - - for (operation of patch) { - /* "/corpusId/valueName" -> ["corpusId", "valueName"] */ - pathArray = operation.path.split("/").slice(1); - if (pathArray[0] != this.jobId) {continue;} - switch(operation.op) { - case "delete": - location.reload(); - break; - case "replace": - switch(pathArray[1]) { - case "description": - updatedElement = document.getElementById("description"); - updatedElement.innerHTML = operation.value; - break; - case "title": - updatedElement = document.getElementById("title"); - updatedElement.innerHTML = operation.value; - break; - default: - break; - } - break; - default: - break; - } - } - } - } - var informationUpdater = new InformationUpdater(CORPUS_ID); -</script> - <div class="col s12 m4"> - <h3 id="title"></h3> - <p id="description"></p> + <h3 id="title">{{ corpus.title }}</h3> + <p id="description">{{ corpus.description }}</p> <h2>Actions:</h2> <!-- Confirm deletion of job with modal dialogue Modal Trigger--> @@ -106,29 +30,33 @@ <div class="row"> <div class="col s12 m6"> <div class="input-field"> - <input disabled value="" id="creation-date" type="text" class="validate"> + <input disabled value="{{ corpus.creation_date.strftime('%m/%d/%Y, %H:%M:%S %p') }}" id="creation-date" type="text" class="validate"> <label for="creation-date">Creation date</label> </div> </div> </div> <span class="card-title">Files</span> - <table> + <table class="highlight responsive-table"> <thead> <tr> - <th style="width: 50%;">Inputs</th> + <th>Filename</th> + <th>Download</th> </tr> </thead> <tbody> - {% for file in files %} + {% for file in corpus.files %} <tr> - <td> - <a href="{{ url_for('main.corpus_download', corpus_id=corpus.id, file=files[file]['path']) }}" class="waves-effect waves-light btn-small"><i class="material-icons left">file_download</i>{{ file }}</a> + <td id="file-{{ file.id }}-filename">{{ file.filename }}</td> + <td id="file-{{ file.id }}-download"> + <a class="waves-effect waves-light btn-small" download href="{{ url_for('main.corpus_download', corpus_id=corpus.id, corpus_file_id=file.id) }}"> + <i class="material-icons">file_download</i> + </a> </td> </tr> {% endfor %} </tbody> </table> + </div> </div> </div> - {% endblock %} diff --git a/app/templates/main/jobs/job.html.j2 b/app/templates/main/jobs/job.html.j2 index 1a43d9dbf5615f82f9ebf3db597d421d6035d6ca..3cdf3933816849ea118c7dd2d53dd6da93d73511 100644 --- a/app/templates/main/jobs/job.html.j2 +++ b/app/templates/main/jobs/job.html.j2 @@ -2,16 +2,16 @@ {% block page_content %} <script> - var job_user_id = {{ job.user_id|tojson|safe }} + var job_user_id = {{ job.user_id }} socket.emit('inspect_user', job_user_id); </script> <script> - var JOB_ID = {{ job.id|tojson|safe }} + var JOB_ID = {{ job.id }} var foreignJobFlag; {% if current_user.id == job.user_id %} - foreignJobFlag = false; + foreignJobFlag = false; {% else %} - foreignJobFlag = true; + foreignJobFlag = true; {% endif %} class InformationUpdater { @@ -25,111 +25,45 @@ } _init() { - var creationDateElement, descriptionElement, endDateElement, - memMbElement, nCoresElement, serviceElement, serviceArgsElement, - serviceVersionElement, statusColor, statusElement, titleElement; - if (foreignJobFlag) { this.job = foreignJobs[this.jobId]; } else { this.job = jobs[this.jobId]; } - // Title - this.setTitle(this.job.title); - // Description - this.setDescription(this.job.description); // Status this.setStatus(this.job.status); - // Creation date - this.setCreationDate(this.job.creation_date); // End date if (this.job.end_date) {this.setEndDate(this.job.end_date);} - // Memory - this.setMemMb(this.job.mem_mb); - // CPU cores - this.setNCores(this.job.n_cores); - // Service - this.setService(this.job.service); - // Service arguments - this.setServiceArgs(this.job.service_args); - // Service version - this.setServiceVersion(this.job.service_version); - - var filesElement, input, inputDownloadElement, inputElement, - inputFilenameElement, inputResultsElement; - - filesElement = document.getElementById("files"); - for (input of this.job.inputs) { - // Data row - inputElement = document.createElement("tr"); - filesElement.append(inputElement); - // Input filename - inputFilenameElement = document.createElement("td"); - inputFilenameElement.id = `input-${input.id}-filename`; - inputElement.append(inputFilenameElement); - // Input download - inputDownloadElement = document.createElement("td"); - inputDownloadElement.id = `input-${input.id}-download`; - inputElement.append(inputDownloadElement); - // Third column for input result file download buttons - inputResultsElement = document.createElement("td"); - inputResultsElement.id = `input-${input.id}-results`; - inputElement.append(inputResultsElement); - this.setInputFilename(input); - this.setInputDownload(input); - this.setInputResults(input); + // Input results + for (let input of this.job.inputs) { + for (let result of input.results) { + this.setResult(result); + } } } - setInputDownload(input) { - var inputDownloadButtonElement, inputDownloadButtonIconElement, - inputDownloadElement; - inputDownloadElement = document.getElementById(`input-${input.id}-download`); - inputDownloadButtonElement = document.createElement("a"); - inputDownloadButtonElement.classList.add("waves-effect", "waves-light", "btn-small"); - inputDownloadButtonElement.href = `${this.jobId}/download?file=${input.filename}`; - inputDownloadButtonElement.setAttribute("download", ""); - inputDownloadButtonIconElement = document.createElement("i"); - inputDownloadButtonIconElement.classList.add("material-icons"); - inputDownloadButtonIconElement.innerText = "file_download"; - inputDownloadButtonElement.append(inputDownloadButtonIconElement); - inputDownloadElement.append(inputDownloadButtonElement); - } - - setInputFilename(input) { - var inputFilenameElement; - inputFilenameElement = document.getElementById(`input-${input.id}-filename`); - inputFilenameElement.innerText = input.filename; - } - _update(patch) { - var input, operation, pathArray; + var pathArray; - for (operation of patch) { + for (let operation of patch) { /* "/jobId/valueName" -> ["jobId", "valueName"] */ pathArray = operation.path.split("/").slice(1); if (pathArray[0] != this.jobId) {continue;} switch(operation.op) { case "add": if (pathArray[1] === "inputs" && pathArray[3] === "results") { - console.log(operation.value); - this.setInputResult(operation.value); + this.setResult(operation.value); } break; case "delete": location.reload(); break; case "replace": - switch(pathArray[1]) { - case "end_date": - this.setEndDate(operation.value); - break; - case "status": - this.setStatus(operation.value); - break; - default: - break; + if (pathArray[1] === "end_date") { + this.setEndDate(operation.value); + } else if (pathArray[1] === "status") { + this.setStatus(operation.value); } break; default: @@ -138,109 +72,43 @@ } } - setTitle(title) { - var titleElement; - titleElement = document.getElementById("title"); - titleElement.innerText = title; - } - - setDescription(description) { - var descriptionElement; - descriptionElement = document.getElementById("description"); - descriptionElement.innerText = description; - } - - setStatus(status) { - var statusColor, statusElement; - statusElement = document.getElementById("status"); - for (statusColor of Object.values(JobList.STATUS_COLORS)) { - statusElement.classList.remove(statusColor); - } - statusElement.classList.add(JobList.STATUS_COLORS[status] || JobList.STATUS_COLORS['default']); - statusElement.innerText = status; - } - - setCreationDate(timestamp) { - var creationDateElement; - creationDateElement = document.getElementById("creation-date"); - creationDateElement.value = new Date(timestamp * 1000).toLocaleString(); - M.updateTextFields(); - } - setEndDate(timestamp) { - var endDateElement; - endDateElement = document.getElementById("end-date"); - endDateElement.value = new Date(timestamp * 1000).toLocaleString(); - M.updateTextFields(); - } - - setMemMb(memMb) { - var memMbElement; - memMbElement = document.getElementById("mem-mb"); - memMbElement.value = memMb; + document.getElementById("end-date").value = new Date(timestamp * 1000).toLocaleString(); M.updateTextFields(); } - setNCores(nCores) { - var nCoresElement; - nCoresElement = document.getElementById("n-cores"); - nCoresElement.value = nCores; - M.updateTextFields(); - } - - setService(service) { - var serviceElement; - serviceElement = document.getElementById("service"); - serviceElement.value = service; - M.updateTextFields(); - } - - setServiceArgs(serviceArgs) { - var serviceArgsElement; - serviceArgsElement = document.getElementById("service-args"); - serviceArgsElement.value = serviceArgs; - M.updateTextFields(); - } - - setServiceVersion(serviceVersion) { - var serviceVersionElement; - serviceVersionElement = document.getElementById("service-version"); - serviceVersionElement.value = serviceVersion; - M.updateTextFields(); - } - - setInputResults(input) { - var result; - for (result of input.results) { - this.setInputResult(result); - } - } - - setInputResult(result) { - var inputResultsElement, resultDownloadButtonElement, + setResult(result) { + var resultsElement, resultDownloadButtonElement, resultDownloadButtonIconElement; - inputResultsElement = document.getElementById(`input-${result.job_input_id}-results`); + resultsElement = document.getElementById(`input-${result.job_input_id}-results`); resultDownloadButtonElement = document.createElement("a"); resultDownloadButtonElement.classList.add("waves-effect", "waves-light", "btn-small"); - var resultFile = `${result.dir}/${result.filename}`; - resultFile = resultFile.substring(resultFile.indexOf("output/")); - resultDownloadButtonElement.href = `${this.jobId}/download?file=${resultFile}`; + resultDownloadButtonElement.href = `/jobs/${this.jobId}/download?ressource_id=${result.id}&ressource_type=result`; resultDownloadButtonElement.innerText = result.filename.split(".").reverse()[0]; resultDownloadButtonElement.setAttribute("download", ""); resultDownloadButtonIconElement = document.createElement("i"); resultDownloadButtonIconElement.classList.add("material-icons", "left"); resultDownloadButtonIconElement.innerText = "file_download"; resultDownloadButtonElement.prepend(resultDownloadButtonIconElement); - inputResultsElement.append(resultDownloadButtonElement); - inputResultsElement.append(" "); + resultsElement.append(resultDownloadButtonElement); + resultsElement.append(" "); + } + + setStatus(status) { + var statusElement; + statusElement = document.getElementById("status"); + statusElement.classList.remove(...Object.values(JobList.STATUS_COLORS)); + statusElement.classList.add(JobList.STATUS_COLORS[status] || JobList.STATUS_COLORS['default']); + statusElement.innerText = status; } } + var informationUpdater = new InformationUpdater(JOB_ID); </script> <div class="col s12 m4"> - <h3 id="title"></h3> - <p id="description"></p> + <h3 id="title">{{ job.title }}</h3> + <p id="description">{{ job.description }}</p> <a class="waves-effect waves-light btn" id="status"></a> <h2>Actions:</h2> <!-- Confirm deletion of job with modal dialogue @@ -269,7 +137,7 @@ <div class="row"> <div class="col s12 m6"> <div class="input-field"> - <input disabled value="" id="creation-date" type="text" class="validate"> + <input disabled value="{{ job.creation_date.strftime('%m/%d/%Y, %H:%M:%S %p') }}" id="creation-date" type="text" class="validate"> <label for="creation-date">Creation date</label> </div> </div> @@ -285,13 +153,13 @@ <div class="row"> <div class="col s12 m6"> <div class="input-field"> - <input disabled value="" id="mem-mb" type="text" class="validate"> + <input disabled value="{{ job.mem_mb }}" id="mem-mb" type="text" class="validate"> <label for="mem-mb">Memory</label> </div> </div> <div class="col s12 m6"> <div class="input-field"> - <input disabled value="" id="n-cores" type="text" class="validate"> + <input disabled value="{{ job.n_cores }}" id="n-cores" type="text" class="validate"> <label for="n-cores">CPU cores</label> </div> </div> @@ -301,19 +169,19 @@ <div class="row"> <div class="col s12 m4"> <div class="input-field"> - <input disabled value="" id="service" type="text" class="validate"> + <input disabled value="{{ job.service }}" id="service" type="text" class="validate"> <label for="service">Service</label> </div> </div> <div class="col s12 m4"> <div class="input-field"> - <input disabled value="" id="service-args" type="text" class="validate"> + <input disabled value="{{ job.service_args|e }}" id="service-args" type="text" class="validate"> <label for="service-args">Service arguments</label> </div> </div> <div class="col s12 m4"> <div class="input-field"> - <input disabled value="" id="service-version" type="text" class="validate"> + <input disabled value="{{ job.service_version }}" id="service-version" type="text" class="validate"> <label for="service-version">Service version</label> </div> </div> @@ -329,12 +197,24 @@ <table class="highlight responsive-table"> <thead> <tr> - <th>File</th> - <th>Input</th> + <th>Filename</th> + <th>Download</th> <th>Results</th> </tr> </thead> - <tbody id="files"></tbody> + <tbody> + {% for input in job.inputs %} + <tr> + <td id="input-{{ input.id }}-filename">{{ input.filename }}</td> + <td id="input-{{ input.id }}-download"> + <a class="waves-effect waves-light btn-small" download href="{{ url_for('main.job_download', job_id=job.id, ressource_id=input.id, ressource_type='input') }}"> + <i class="material-icons">file_download</i> + </a> + </td> + <td id="input-{{ input.id }}-results"></td> + </tr> + {% endfor %} + </tbody> </table> </div> </div> diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index af07577710e6c286f86e5d9b6b7b6a4ee7849143..85c9486d43bbef53474ff197d38223a67ceb38de 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,6 +1,10 @@ #!/bin/sh -./wait-for-it/wait-for-it.sh db:5432 --strict --timeout=0 +echo "Waiting for db..." +wait-for-it/wait-for-it.sh db:5432 --strict --timeout=0 +echo "Waiting for redis..." +wait-for-it/wait-for-it.sh redis:6379 --strict --timeout=0 + if [ $# -eq 0 ] then venv/bin/python -u opaque.py