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

Submit: První verze odevzdávání

parent cb67258c
No related branches found
No related tags found
1 merge request!9WIP: Zárodek uživatelské části webu a submitování
...@@ -18,3 +18,6 @@ SESSION_COOKIE_NAME = 'mo_session' ...@@ -18,3 +18,6 @@ SESSION_COOKIE_NAME = 'mo_session'
# URL, na kterém aplikace běží # URL, na kterém aplikace běží
WEB_ROOT = 'https://mo.mff.cuni.cz/osmo-test/' WEB_ROOT = 'https://mo.mff.cuni.cz/osmo-test/'
# Maximální velikost uploadu. Pozor, je omezena i konfigurací Nginxu.
MAX_CONTENT_LENGTH = 16777216
import datetime
import multiprocessing
import os
import pikepdf
import secrets
import werkzeug.utils
import mo.db as db
from mo.util import logger
class SubmitException(RuntimeError):
pass
class Submitter:
submit_dir: str
def __init__(self, instance_path: str = 'data'):
self.submit_dir = os.path.join(instance_path, 'submits')
def submit_paper(self, paper: db.Paper, tmpfile: str):
logger.info(f'Submit: Zpracovávám file={tmpfile} for=#{paper.for_user_obj.user_id} by=#{paper.uploaded_by_obj.user_id} type={paper.type.name}')
t_start = datetime.datetime.now()
try:
self._do_submit(paper, tmpfile)
duration = (datetime.datetime.now() - t_start).total_seconds()
logger.info(f'Submit: Hotovo: file={paper.file_name} pages={paper.pages} bytes={paper.bytes} time={duration:.3f}')
except SubmitException as e:
duration = (datetime.datetime.now() - t_start).total_seconds()
logger.info(f'Submit: Chyba: {e} (time={duration:.3f})')
raise
def _create_file_name(self, paper: db.Paper) -> str:
user_dir = os.path.join(str(paper.task.round.year), str(paper.for_user_obj.user_id))
sub_user_dir = os.path.join(self.submit_dir, user_dir)
os.makedirs(sub_user_dir, exist_ok=True)
secure_task_code = werkzeug.utils.secure_filename(paper.task.code)
while True:
nonce = secrets.token_hex(8)
file_name = f'{secure_task_code}-{paper.type.name[:3]}-{nonce}.pdf'
if not os.path.lexists(os.path.join(sub_user_dir, file_name)):
break
logger.warning(f'Retrying file creation for {sub_user_dir}/{file_name}')
return os.path.join(user_dir, file_name)
def _do_submit(self, paper: db.Paper, tmpfile: str):
# Zpracování PDF spustíme v samostatném procesu, aby bylo dostatečně oddělené
# FIXME: Omezit paměť apod.
pipe_rx, pipe_tx = multiprocessing.Pipe(duplex=False)
proc = multiprocessing.Process(name='submit', target=Submitter._process_pdf, args=(tmpfile, pipe_tx))
proc.start()
pipe_tx.close()
if not pipe_rx.poll(10):
proc.terminate()
proc.join()
raise SubmitException('Timeout při zpracování PDF.')
try:
result = pipe_rx.recv()
except EOFError:
result = None
proc.terminate()
proc.join()
assert proc.exitcode is not None
if proc.exitcode != 0:
raise SubmitException(f'Interní chyba při zpracování PDF: exit code {proc.exitcode}.')
if not result:
raise SubmitException('Interní chyba při zpracování PDF: EOF.')
if 'error' in result:
logger.info('Submit: PDF error: ' + result['error'])
raise SubmitException('Soubor není korektní PDF.')
paper.bytes = os.path.getsize(tmpfile)
paper.pages = result['pages']
paper.file_name = self._create_file_name(paper)
# FIXME: fsync?
dest = os.path.join(self.submit_dir, paper.file_name)
os.rename(tmpfile, dest)
# Zpracování PDF běží v samostatném procesu, výsledek pošle jako slovník rourou.
def _process_pdf(tmpfile, pipe):
try:
with pikepdf.open(tmpfile) as pdf:
pages = len(pdf.pages)
except pikepdf.PdfError as e:
pipe.send({
"error": str(e),
})
return
pipe.send({
"pages": pages,
})
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block body %}
<h2>Úloha {{ task.code }} {{ task.name }}</h2>
<p><a href='{{ url_for('user_contest', id=contest.contest_id) }}'>Zpět na seznam úloh</a>
<p>FIXME: Povídání, design. Typ souborů a maximální velikost.
{{ wtf.quick_form(form, form_type='horizontal') }}
{% endblock %}
from flask import render_template, request, g, redirect, url_for from flask import render_template, request, g, redirect, url_for, flash
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
import werkzeug.exceptions import flask_wtf.file
import wtforms import os
import wtforms.validators as validators import secrets
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from typing import Optional from typing import Optional
import werkzeug.exceptions
import wtforms
import wtforms.validators as validators
import mo.util
import mo.db as db import mo.db as db
import mo.submit
import mo.util
from mo.web import app, get_request_time from mo.web import app, get_request_time
...@@ -42,12 +46,24 @@ def get_contest(id: int) -> db.Contest: ...@@ -42,12 +46,24 @@ def get_contest(id: int) -> db.Contest:
raise werkzeug.exceptions.NotFound() raise werkzeug.exceptions.NotFound()
# FIXME: Časem chceme účastníky pustit i v jiných stavech # FIXME: Časem chceme účastníky pustit i v jiných stavech
# FIXME: A také místo generického Forbidden říci něco konkrétnějšího
# (je možné, že se sem účastník dostal reloadem stránky po konci contestu)
if contest.round.state != db.RoundState.running: if contest.round.state != db.RoundState.running:
raise werkzeug.exceptions.Forbidden() raise werkzeug.exceptions.Forbidden()
return contest return contest
def get_task(contest: db.Contest, id: int) -> db.Task:
task = db.get_session().query(db.Task).get(id)
# Nezapomeňme zkontrolovat, že úloha patří do soutěže :)
if not task or task.round_id != contest.round_id:
raise werkzeug.exceptions.NotFound()
return task
@app.route('/user/contest/<int:id>/') @app.route('/user/contest/<int:id>/')
def user_contest(id: int): def user_contest(id: int):
contest = get_contest(id) contest = get_contest(id)
...@@ -61,8 +77,6 @@ def user_contest(id: int): ...@@ -61,8 +77,6 @@ def user_contest(id: int):
joinedload(db.Solution.last_feedback_obj)) joinedload(db.Solution.last_feedback_obj))
.all()) .all())
print(task_sols)
return render_template( return render_template(
'user_contest.html', 'user_contest.html',
contest=contest, contest=contest,
...@@ -70,3 +84,60 @@ def user_contest(id: int): ...@@ -70,3 +84,60 @@ def user_contest(id: int):
db=db, # kvůli hodnotám enumů db=db, # kvůli hodnotám enumů
when=get_request_time(), when=get_request_time(),
) )
class SubmitForm(FlaskForm):
file = flask_wtf.file.FileField("Soubor", validators=[flask_wtf.file.FileRequired()])
submit = wtforms.SubmitField('Odevzdat')
@app.route('/user/contest/<int:contest_id>/task/<int:task_id>/', methods=('GET', 'POST'))
def user_contest_task(contest_id: int, task_id: int):
contest = get_contest(contest_id)
task = get_task(contest, task_id)
form = SubmitForm()
if form.validate_on_submit():
# FIXME: Tohle je pomalé, dělá se tu zbytečná další kopie dat.
# Nicméně werkzeugu by měla jít podstrčit stream factory,
# která bude vyrábět streamy rovnou uložené v našem tmp.
tmp_name = secrets.token_hex(16)
tmp_path = os.path.join(app.instance_path, 'tmp', tmp_name)
form.file.data.save(tmp_path)
paper = db.Paper(task=task, for_user_obj=g.user, uploaded_by_obj=g.user, type=db.PaperType.solution)
submitter = mo.submit.Submitter(instance_path=app.instance_path)
try:
submitter.submit_paper(paper, tmp_path)
except mo.submit.SubmitException as e:
flash(f'Chyba: {e}', 'danger')
# FIXME: Tady nemažeme tmpfile, zatím si ho chceme nechat pro analýzu.
return redirect(url_for('user_contest_task', contest_id=contest_id, task_id=task_id))
sess = db.get_session()
sess.add(paper)
# FIXME: Bylo by hezké použít INSERT ... ON CONFLICT UPDATE
# (SQLAlchemy to umí, ale ne přes ORM, jen core rozhraním)
sol = (sess.query(db.Solution)
.filter_by(task=task, user=g.user)
.with_for_update()
.one_or_none())
if sol is None:
sol = db.Solution(task=task, user=g.user)
sess.add(sol)
sol.last_submit_obj = paper
sess.commit()
flash('Úloha odevzdána', 'success')
return redirect(url_for('user_contest', id=contest_id))
return render_template(
'user_contest_task.html',
contest=contest,
task=task,
when=get_request_time(),
form=form,
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment