Skip to content
Snippets Groups Projects
Commit 59252e28 authored by Jan Prachař's avatar Jan Prachař
Browse files

Soutěžící může odevzdávat řešení jako sadu obrázků

parent 16d5e569
No related branches found
No related tags found
1 merge request!58Submit images
......@@ -2,8 +2,11 @@ import datetime
import multiprocessing
import os
import pikepdf
from typing import Any
import PIL
import tempfile
from typing import Any, BinaryIO, List
import werkzeug.utils
import zipfile
from img2pdf import img2pdf
import mo.db as db
......@@ -11,6 +14,9 @@ import mo.util
from mo.util import logger
allowed_extensions = ('.pdf', '.png', '.jpg', 'jpeg')
class SubmitException(RuntimeError):
pass
......@@ -21,6 +27,75 @@ class Submitter:
def __init__(self):
self.submit_dir = mo.util.data_dir('submits')
def submit_images(self, paper: db.Paper, files: List[str], names: List[str]):
logger.info(f'Submit: Zpracovávám images={files} 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_images(paper, files, names)
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()
zip_tmpfile = tempfile.NamedTemporaryFile(suffix='.zip', mode='w+b')
with zipfile.ZipFile(zip_tmpfile, mode='w') as zip:
for i, f in enumerate(files, start=0):
fn = werkzeug.utils.secure_filename(names[i])
zip.write(filename=f, arcname=fn)
preserved_as = mo.util.link_to_dir(zip_tmpfile.name, mo.util.data_dir('errors'), prefix='submit-')
logger.info(f'Submit: Chyba: {e} (time={duration:.3f}), uloženo do {preserved_as}')
zip_tmpfile.close()
raise
def _do_submit_images(self, paper: db.Paper, files: List[str], names: List[str]):
# Zpracování obrázků spustíme v samostatném procesu, aby bylo dostatečně oddělené
with tempfile.NamedTemporaryFile(prefix='fpdf-', mode='w+b') as tmpfile:
pipe_rx, pipe_tx = multiprocessing.Pipe(duplex=False)
proc = multiprocessing.Process(name='submit',
target=Submitter._process_images,
args=(tmpfile, files, names, pipe_tx))
proc.start()
pipe_tx.close()
if not pipe_rx.poll(60):
proc.terminate()
proc.join()
raise SubmitException('Timeout při zpracování obrázků.')
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í obrázků: Exit code {proc.exitcode}.')
if not result:
raise SubmitException('Interní chyba při zpracování obrázků: EOF.')
if 'error' in result:
logger.info('Submit: %s', result['error'])
raise SubmitException(result['error'])
else:
paper.bytes = result['bytes']
paper.pages = len(files)
self._file_paper(paper, tmpfile.name)
def _process_images(tmpfile: BinaryIO, files: List[str], names: List[str], pipe):
result: Any = {}
a4inpt = (img2pdf.mm_to_pt(210),img2pdf.mm_to_pt(297))
try:
layout_fun = img2pdf.get_layout_fun(a4inpt)
tmpfile.write(img2pdf.convert(files, layout_fun=layout_fun))
result['bytes'] = tmpfile.tell()
except Exception as e:
result['error'] = f'Selhala konverze obrázků do PDF: {e}'
pipe.send(result)
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()
......
......@@ -39,7 +39,7 @@
{% endif %}
</p>
{% if contest.ct_can_submit() %}
<p>Řešení odevzdávejte ve formátu PDF jako soubor o velikosti maximálně
<p>Řešení úloh odevzdávejte jako sadu obrázků (JPG nebo PNG) nebo jeden soubor ve formátu PDF. Celková velikost může být maximálně
{{ max_submit_size // 1048576 }} MB.
Pokud si s tvorbou PDF nevíte rady, zkuste se podívat do <a href='https://docs.google.com/document/d/1XXk7Od-ZKtfmfNa-9FpFjUqmy0Ekzf2-2q3EpSWyn1w/edit?usp=sharing'>návodu</a>.
{% endif %}
......@@ -52,14 +52,14 @@ Pokud si s tvorbou PDF nevíte rady, zkuste se podívat do <a href='https://docs
<p>Soutěž se nachází v neznámém stavu. To by se nemělo stát :)
{% endif %}
{% if contest.ct_task_statement_available() %}
<p>Můžete si stáhnout <a href='{{ url_for('user_task_statement', id=contest.contest_id) }}'>zadání úloh</a>.
{% endif %}
{% if state != RoundState.preparing %}
<h3>Úlohy</h3>
{% if contest.ct_task_statement_available() %}
<p>Můžete si stáhnout <a href='{{ url_for('user_task_statement', id=contest.contest_id) }}'>zadání úloh</a>.
{% endif %}
<table class="table table-bordered table-hover">
<thead>
<tr>
......
......@@ -23,6 +23,7 @@
{% if state == RoundState.running %}
{% if contest.ct_can_submit() %}
<h3>Odevzdat řešení</h3>
<p>Pokud nahráváte obrázky, vždy vyberte všechny listy, které chcete odevzdávat.
{% if round.ct_submit_end and g.now > round.ct_submit_end %}
<p class="alert alert-danger">Pozor, odevzdáváte po termínu, uplynul {{ round.ct_submit_end|time_and_timedelta }}.
Vaše řešení nemusí být hodnoceno. Doporučujeme využít políčko pro poznámku a vysvětlit situaci.
......@@ -31,7 +32,27 @@
řešením správným. V tom případě však uveďte do poznámky, proč jste řešení
nahradili (např. nahráli jste omylem řešení jiné úlohy).
{% endif %}
{{ wtf.quick_form(form, form_type='basic', button_map={'submit': 'primary'}) }}
<form action="" method="post" class="form" role="form" enctype="multipart/form-data">
{{ form.csrf_token }}
<div class="form-group required">
<label class="control-label" for="file">Vypracované řešení</label>
<input id="file" multiple="" name="file" required="" type="file" accept=".png,.jpeg,.jpg,.pdf">
<p class="help-block">Vyberte jeden PDF soubor, nebo několik obrázků (JPG, PNG).</p>
</div>
{{ wtf.form_field(form.note) }}
{{ wtf.form_field(form.submit, class='btn btn-primary') }}
</form>
<h4>Maximální velikost</h4>
<p>Celková velikost všech souborů, které najednou nahráváte, může
být maximálně <b>{{ max_submit_size // 1048576 }} MB</b>. Pokud po
odeslání formuláře dostanete odpověď <i>Request Entity Too
Large</i>, přesáhli jste tento limit. Zmenšete prosím velikost
odesílaných obrázků (například foťte z větší dálky a výsledek
ořízněte) a zkuste to znovu.
{% else %}
<p>Již není možné odevzdat řešení, termín na odevzdávání vypršel.</p>
{% endif %}
......
from flask import render_template, jsonify, g, redirect, url_for, flash
from flask_wtf import FlaskForm
import flask_wtf.file
import os
from sqlalchemy import and_
from sqlalchemy.orm import joinedload
from typing import List, Tuple
......@@ -154,7 +155,7 @@ def user_task_statement(id: int):
class SubmitForm(FlaskForm):
file = flask_wtf.file.FileField("Soubor", validators=[flask_wtf.file.FileRequired()], render_kw={'autofocus': True})
file = wtforms.MultipleFileField("Soubor", validators=[validators.required()])
note = wtforms.TextAreaField("Poznámka", description="Zde můžete něco vzkázat organizátorům soutěže.")
submit = wtforms.SubmitField('Odevzdat')
......@@ -175,12 +176,44 @@ def user_contest_task(contest_id: int, task_id: int):
form = SubmitForm()
if contest.ct_can_submit() and form.validate_on_submit():
file = form.file.data.stream
paper = db.Paper(task=task, for_user_obj=g.user, uploaded_by_obj=g.user, type=db.PaperType.solution, note=form.note.data)
submitter = mo.submit.Submitter()
files = request.files.getlist(form.file.name)
sizeok = [f.filename.lower().endswith('.pdf')
or os.path.getsize(f.stream.name) <= 10*1024*1024
for f in files]
if not files:
flash('Neodeslali jste žádné soubory.', 'danger')
elif len(files) > 1 and any(f.filename.lower().endswith('.pdf') for f in files):
flash('Odevzdávat můžete pouze jeden PDF soubor.', 'danger')
elif not all(f.filename.lower().endswith(mo.submit.allowed_extensions) for f in files):
invalid = []
for f in files:
if not f.filename.lower().endswith(mo.submit.allowed_extensions):
invalid.append(f.filename)
flash(
'Odevzdávat můžete jen obrázky a PDF soubory. Nepovolené soubory: {}'.format(
', '.join(invalid)),
'danger')
elif not all(sizeok):
for i, s in enumerate(sizeok, start=0):
if not s:
flash(f'Obrázek {files[i].filename} je větší než 10 MB. Zmenšete ho prosím.', 'danger')
else:
try:
submitter.submit_paper(paper, file.name)
if not files[0].filename.lower().endswith('.pdf'):
submitter.submit_images(
paper,
[f.stream.name for f in files],
[f.filename for f in files])
else:
submitter.submit_paper(paper, files[0].stream.name)
except mo.submit.SubmitException as e:
flash(f'Chyba: {e}', 'danger')
return redirect(url_for('user_contest_task', contest_id=contest_id, task_id=task_id))
......@@ -224,6 +257,7 @@ def user_contest_task(contest_id: int, task_id: int):
papers=papers,
form=form,
messages=messages,
max_submit_size=config.MAX_CONTENT_LENGTH,
)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment