Skip to content
Snippets Groups Projects
Commit b1a4234a authored by Martin Mareš's avatar Martin Mareš
Browse files

UI k jobům

parent 326491c2
No related branches found
No related tags found
2 merge requests!14Asynchronní joby,!9WIP: Zárodek uživatelské části webu a submitování
...@@ -470,6 +470,17 @@ class JobState(MOEnum): ...@@ -470,6 +470,17 @@ class JobState(MOEnum):
done = auto() done = auto()
failed = auto() failed = auto()
def friendly_name(self) -> str:
return job_state_names[self]
job_state_names = {
JobState.ready: 'čeká na spuštění',
JobState.running: 'zpracovává se',
JobState.done: 'dokončen',
JobState.failed: 'selhal',
}
class Job(Base): class Job(Base):
__tablename__ = 'jobs' __tablename__ = 'jobs'
......
...@@ -127,6 +127,7 @@ import mo.web.menu ...@@ -127,6 +127,7 @@ import mo.web.menu
import mo.web.misc import mo.web.misc
import mo.web.org import mo.web.org
import mo.web.org_contest import mo.web.org_contest
import mo.web.org_jobs
import mo.web.org_place import mo.web.org_place
import mo.web.org_round import mo.web.org_round
import mo.web.org_users import mo.web.org_users
......
from flask import render_template, g, redirect, url_for, flash
from flask_wtf.form import FlaskForm
from sqlalchemy.orm import joinedload
import werkzeug.exceptions
import wtforms
import mo
import mo.db as db
from mo.jobs import TheJob
from mo.web import app
import mo.web.util
class JobDeleteForm(FlaskForm):
delete_job_id = wtforms.IntegerField()
delete = wtforms.SubmitField('Smazat')
@app.route('/org/jobs/', methods=('GET', 'POST'))
def org_jobs():
sess = db.get_session()
form_delete_job = JobDeleteForm()
if form_delete_job.validate_on_submit():
tj = TheJob(form_delete_job.delete_job_id.data)
job = tj.load()
if not job:
flash('Dávka mezitím zmizela.', 'success')
elif not (job.user_id == g.user.user_id or g.user.is_admin):
flash('Tuto dávku nemáte právo smazat', 'danger')
elif job.state == db.JobState.running:
flash('Běžící dávku nelze smazat', 'danger')
else:
tj.remove_loaded()
flash('Dávka smazána', 'success')
return redirect(url_for('org_jobs'))
job_query = (sess.query(db.Job)
.options(joinedload(db.Job.user)))
if not g.user.is_admin:
job_query = job_query.filter_by(user=g.user)
jobs = job_query.order_by(db.Job.created_at.desc()).all()
return render_template(
'org_jobs.html',
jobs=jobs,
db=db,
form_delete_job=form_delete_job,
)
@app.route('/org/jobs/<int:id>/output')
def org_job_output(id: int):
sess = db.get_session()
job = sess.query(db.Job).get(id)
if job is None:
return werkzeug.exceptions.NotFound()
if not (job.user_id == g.user.user_id or g.user.is_org):
return werkzeug.exceptions.Forbidden()
if job.state != db.JobState.done or job.out_file is None:
return werkzeug.exceptions.NotFound()
return mo.web.util.send_job_result(job)
{% extends "base.html" %}
{% block body %}
<h2>Dávky ke zpracování</h2>
<p>Činnosti, jejichž zpracování může trvat delší dobu, se zpracovávají
dávkově. To se týká třeba stahování více řešení najednou – když si je
objednáte, vytvoří se dávka. Až dávka doběhne, můžete si zde stáhnout výsledek.
Po nějaké době výsledek vyprší a je automaticky smazán. Budeme rádi, když
dávku po stažení výstupu smažete sami – šetří to místo na serveru.
<h3>Seznam dávek</h3>
{% if jobs %}
<table class=data>
<thead>
<tr>
<th>ID
<th>Název
<th>Spuštěno
<th>Stav
<th>Vyprší
{% if g.user.is_admin %}<th>Vlastník{% endif %}
<th>Akce
<tbody>
{% for j in jobs %}
<tr class="job-{{ j.state.name }}">
<td>{{ j.job_id }}
<td>{{ j.description }}
<td>{{ j.created_at|timeformat }}
<td><b>{{ j.state.friendly_name() }}</b>{% if j.finished_at != None %} {{ j.finished_at|timedelta }}{% endif %}
<td>{% if j.expires_at %}{{ j.expires_at|timedelta}}{% endif %}
{% if g.user.is_admin %}<td>{{ j.user.full_name() }}{% endif %}
<td>
<div class='btn-group'><form action="" method="POST" class="btn-group">
{{ form_delete_job.csrf_token() }}
{% if j.out_file %}
<a class='btn btn-xs btn-primary' href='{{ url_for('org_job_output', id=j.job_id) }}'>Výsledek</a>
{% endif %}
<input type="hidden" name="delete_job_id" value="{{ j.job_id }}">
<button type="submit" class="btn btn-xs btn-danger">Smazat</button>
</form></div>
{% endfor %}
</table>
{% else %}
<p>Žádné dávky nejsou nyní naplánované.
{% endif %}
{% endblock %}
...@@ -8,6 +8,7 @@ import werkzeug.utils ...@@ -8,6 +8,7 @@ import werkzeug.utils
import wtforms import wtforms
import mo.db as db import mo.db as db
import mo.jobs
from mo.util import logger from mo.util import logger
from mo.web import app from mo.web import app
...@@ -72,3 +73,19 @@ def send_task_paper(paper: db.Paper) -> Response: ...@@ -72,3 +73,19 @@ def send_task_paper(paper: db.Paper) -> Response:
else: else:
logger.error(f'Soubor {file} je v papers, ale ve FS neexistuje') logger.error(f'Soubor {file} je v papers, ale ve FS neexistuje')
raise werkzeug.exceptions.NotFound() raise werkzeug.exceptions.NotFound()
def send_job_result(job: db.Job) -> Response:
assert job.out_file is not None
file = mo.jobs.job_file_path(job.out_file)
if file.endswith('.zip'):
type = 'application/zip'
else:
type = 'application/binary'
if os.path.isfile(file):
return send_file(file, mimetype=type)
else:
logger.error(f'Soubor {file} je výsledkem jobu, ale ve FS neexistuje')
raise werkzeug.exceptions.NotFound()
...@@ -160,3 +160,29 @@ nav#main-menu a.active { ...@@ -160,3 +160,29 @@ nav#main-menu a.active {
.sol-late { .sol-late {
color: red; color: red;
} }
/* Jobs */
table.data tbody tr.job-running {
background-color: #f8f;
}
table.data tbody tr.job-running:hover {
background-color: #a6a;
}
table.data tbody tr.job-done {
background-color: #8f8;
}
table.data tbody tr.job-done:hover {
background-color: #6a6;
}
table.data tbody tr.job-failed {
background-color: #f88;
}
table.data tbody tr.job-failed:hover {
background-color: #a66;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment