diff --git a/db/db.ddl b/db/db.ddl index c1da8be2afc3886caa95b26be204b6cd6e2516e2..57cfda34f91ae7969d4648b6270a2c1737d7d112 100644 --- a/db/db.ddl +++ b/db/db.ddl @@ -163,7 +163,8 @@ CREATE TABLE papers ( pages int DEFAULT NULL, -- počet stránek bytes int DEFAULT NULL, -- velikost souboru file_name varchar(255) NOT NULL, -- relativní cesta k souboru - note text NOT NULL DEFAULT '' -- komentář uploadujícího + note text NOT NULL DEFAULT '', -- komentář uploadujícího + broken bool NOT NULL DEFAULT false -- poničené PDF přijaté s varováním ); CREATE INDEX papers_for_task_index ON papers (for_task); diff --git a/db/upgrade-20210126.sql b/db/upgrade-20210126.sql new file mode 100644 index 0000000000000000000000000000000000000000..f3e76a2e6027e9203adac709a2c6e4065b52c0c5 --- /dev/null +++ b/db/upgrade-20210126.sql @@ -0,0 +1,4 @@ +SET ROLE 'mo_osmo'; + +ALTER TABLE papers + ADD COLUMN broken bool NOT NULL DEFAULT false; -- poničené PDF přijaté s varováním diff --git a/mo/db.py b/mo/db.py index f8624591138bd68276241cda0f98956a0b6d594f..7a723e522c0599fbf3a2c1e6e36d1f6e2a38e978 100644 --- a/mo/db.py +++ b/mo/db.py @@ -419,6 +419,7 @@ class Paper(Base): bytes = Column(Integer) file_name = Column(String(255), nullable=False) note = Column(Text, nullable=False, server_default=text("''::text")) + broken = Column(Boolean, nullable=False, server_default=text("false")) task = relationship('Task') for_user_obj = relationship('User', primaryjoin='Paper.for_user == User.user_id') diff --git a/mo/submit.py b/mo/submit.py index 9f896b00a8de954b9fb0ddded291216b8af7c182..9dd9ca3d14ee5df28bedd7e392e49bc5480b6913 100644 --- a/mo/submit.py +++ b/mo/submit.py @@ -2,6 +2,7 @@ import datetime import multiprocessing import os import pikepdf +from typing import Any import werkzeug.utils import mo.db as db @@ -26,7 +27,7 @@ class Submitter: 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}') + logger.info(f'Submit: Hotovo: file={paper.file_name} pages={paper.pages} bytes={paper.bytes} time={duration:.3f} broken={1 if paper.broken else 0}') except SubmitException as e: duration = (datetime.datetime.now() - t_start).total_seconds() preserved_as = mo.util.link_to_dir(tmpfile, mo.util.data_dir('errors'), prefix='submit-') @@ -71,24 +72,43 @@ class Submitter: 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.') + logger.info('Submit: PDF error: %s', result['error']) + if result['pdf-like']: + logger.info('Submit: Soubor akceptován s varováním') + paper.broken = True + else: + raise SubmitException('Soubor není korektní PDF.') + else: + paper.pages = result['pages'] paper.bytes = os.path.getsize(tmpfile) - paper.pages = result['pages'] self._file_paper(paper, tmpfile) # Zpracování PDF běží v samostatném procesu, výsledek pošle jako slovník rourou. - def _process_pdf(tmpfile, pipe): + def _process_pdf(tmpfile: str, pipe): + result: Any = {} try: with pikepdf.open(tmpfile, attempt_recovery=False) as pdf: - pages = len(pdf.pages) + result['pages'] = len(pdf.pages) except pikepdf.PdfError as e: - pipe.send({ - "error": str(e), - }) - return - - pipe.send({ - "pages": pages, - }) + result['error'] = str(e) + result['pdf-like'] = Submitter._looks_like_pdf(tmpfile) + pipe.send(result) + + def _looks_like_pdf(tmpfile: str) -> bool: + """PDFka, která nezvládne otevřít QPDF, jsme ochotni akceptovat s warningem, + pokud začátek i konec souboru vypadá jako PDF.""" + + with open(tmpfile, 'rb') as f: + header = f.read(5) + if header != b'%PDF-': + return False + + f.seek(0, 2) + size = f.tell() + if size < 100: + return False + + f.seek(-100, 2) + trailer = f.read(100) + return b'startxref' in trailer and b'%%EOF' in trailer diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 44fa0a58bbc437cf2099aa1ec11b04090014bcd0..6d76ffb39ba5dcb2a78909a16b0ed1211462d4d2 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -662,9 +662,16 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option sess.commit() if type == db.PaperType.solution: - flash('Řešení odevzdáno', 'success') + prefix = 'Řešení' else: - flash('Opravené řešení odevzdáno', 'success') + prefix = 'Opravené řešení' + + if paper.broken: + flash(prefix + ' není korektní PDF, ale přesto jsme ho přijali a pokusíme se ho zpracovat. ' + + 'Zkontrolujte prosím, že se na vašem počítači zobrazuje správně.', + 'warning') + else: + flash(prefix + ' odevzdáno', 'success') return redirect(self_url) # Najdeme řešení úlohy (nemusí existovat) diff --git a/mo/web/templates/base.html b/mo/web/templates/base.html index b87201488a68818bf73c55ee3e08391c563453b5..11f35b2b7af3464a74f542ac8b92f91cababf17a 100644 --- a/mo/web/templates/base.html +++ b/mo/web/templates/base.html @@ -3,7 +3,7 @@ <head> <title>Odevzdávací systém MO: {% block title %}{% endblock %}</title> <link rel=stylesheet href="{{ url_for('static', filename='bootstrap.min.css') }}?v=2" type='text/css' media=all> - <link rel=stylesheet href="{{ url_for('static', filename='mo.css') }}?v=7" type='text/css' media=all> + <link rel=stylesheet href="{{ url_for('static', filename='mo.css') }}?v=8" type='text/css' media=all> {% block head %}{% endblock %} </head> <body> diff --git a/mo/web/templates/org_contest_solutions.html b/mo/web/templates/org_contest_solutions.html index 5606f4e6c1966a19b0b43315b419cd457716760e..a7b030972c37f1dd51ce7d44641dae5cc1278c8e 100644 --- a/mo/web/templates/org_contest_solutions.html +++ b/mo/web/templates/org_contest_solutions.html @@ -51,8 +51,8 @@ konkrétní úlohu. Symbol <b>+</b> značí, že existuje více verzí dostupný {% if sol.final_submit_obj %} {% set p = sol.final_submit_obj %} {% set late = p.check_deadline(round) %} - <td class="sol {% if late %}sol-late{% endif %}"> - <a href="{{ paper_link(u, p) }}" title="{{ p.uploaded_at|timeformat }} - {{ p.pages|inflected('stránka', 'stránky', 'stránek') }}{% if late %} - {{ late }}{% endif %}">🖺</a> + <td class="sol{% if late or p.broken %} sol-warn{% endif %}"> + <a href="{{ paper_link(u, p) }}" title="{{ p.uploaded_at|timeformat }}{% if p.broken %} - nekorektní PDF{% endif %}{% if p.pages != None %} - {{ p.pages|inflected('stránka', 'stránky', 'stránek') }}{% endif %}{% if late %} - {{ late }}{% endif %}">🖺</a> {% set key = (u.user_id, task.task_id, "solution") %} {% if key in paper_counts and paper_counts[key] > 1 %} <b title="Celkem {{ paper_counts[key]|inflected('verze', 'verze', 'verzí') }}">+</b> @@ -60,15 +60,17 @@ konkrétní úlohu. Symbol <b>+</b> značí, že existuje více verzí dostupný {% else %} <td class="sol"> {% endif %} - <td class="sol"> - {% if sol.final_feedback_obj %} - {% set p = sol.final_feedback_obj %} - <a href="{{ paper_link(u, p) }}" title="{{ p.uploaded_at|timeformat }} - {{ p.pages|inflected('stránka', 'stránky', 'stránek') }}">🖺</a> + {% if sol.final_feedback_obj %} + {% set p = sol.final_feedback_obj %} + <td class="sol{% if p.broken %} sol-warn{% endif %}"> + <a href="{{ paper_link(u, p) }}" title="{{ p.uploaded_at|timeformat }}{% if p.broken %} - nekorektní PDF{% endif %}{% if p.pages != None %} - {{ p.pages|inflected('stránka', 'stránky', 'stránek') }}{% endif %}">🖺</a> {% set key = (u.user_id, task.task_id, "feedback") %} {% if key in paper_counts and paper_counts[key] > 1 %} <b title="Celkem {{ paper_counts[key]|inflected('verze', 'verze', 'verzí') }}">+</b> {% endif %} - {% endif %} + {% else %} + <td class="sol"> + {% endif %} <td class="sol"> {% if sol.points is not none %} {{ sol.points }} diff --git a/mo/web/templates/org_submit_list.html b/mo/web/templates/org_submit_list.html index 489b3aac833b838fe04a344df9210254639e3a3c..ea618ebd49a3e419cd471ba80a8b62f11548c6d8 100644 --- a/mo/web/templates/org_submit_list.html +++ b/mo/web/templates/org_submit_list.html @@ -48,11 +48,11 @@ {% for p in sol_papers %} {% set late = p.check_deadline(sc.round) %} <tr{% if p.paper_id == active_sol_id %} class='sol-active'{% endif %}> - <td{% if late %} class='sol-late'{% endif %}>{{ p.uploaded_at|timeformat }} - <td>{{ p.pages }} - <td>{{ p.bytes }} + <td{% if late %} class='sol-warn'{% endif %}>{{ p.uploaded_at|timeformat }} + <td>{% if p.broken %}nekorektní PDF{% else %}{{ p.pages|or_dash }}{% endif %} + <td>{{ p.bytes|or_dash }} <td>{{ p.uploaded_by_obj|user_link }} - <td>{% if late %}<span class='sol-late'>({{ late }})</span> {% endif %}{{ p.note }} + <td>{% if late %}<span class='sol-warn'>({{ late }})</span> {% endif %}{{ p.note }} <td><div class="btn-group"> <a class='btn btn-xs btn-primary' href='{{ paper_link(p) }}'>Stáhnout</a> {% if p.paper_id != active_sol_id and set_final_form %} @@ -98,8 +98,8 @@ jen finální opravu a ani se nedozví, kolik různých verzí existovalo. {% for p in fb_papers %} <tr{% if p.paper_id == active_fb_id %} class='sol-active'{% endif %}> <td>{{ p.uploaded_at|timeformat }} - <td>{{ p.pages }} - <td>{{ p.bytes }} + <td>{% if p.broken %}nekorektní PDF{% else %}{{ p.pages|or_dash }}{% endif %} + <td>{{ p.bytes|or_dash }} <td>{{ p.uploaded_by_obj|user_link }} <td>{{ p.note }} <td><div class="btn-group"> diff --git a/mo/web/templates/parts/org_solution_table.html b/mo/web/templates/parts/org_solution_table.html index 93d622e8d1e99aa631956006292bc7480753c0c0..3e7184b1ddeb6e8e4f362044ef6ce6932b25ab84 100644 --- a/mo/web/templates/parts/org_solution_table.html +++ b/mo/web/templates/parts/org_solution_table.html @@ -4,7 +4,7 @@ finální řešení, finální oprava a přidělené body. Historii všech odevz finální (ve výchozím stavu poslední nahrané).{% endif %} </i></p> -<p><i>Legenda k symbolům: <span class='sol-late'><b>⚠</b></span> odevzdané po termínu, +<p><i>Legenda k symbolům: <span class='sol-warn'><b>⚠</b></span> odevzdané po termínu, <b>🛈</b> nahráno někým jiným, než řešitelem, <b>+</b> existuje více verzí. Symboly po najetí myší zobrazí bližší informace. </i></p> @@ -34,9 +34,14 @@ finální (ve výchozím stavu poslední nahrané).{% endif %} <td>{% if sol.final_submit_obj %} {% set p = sol.final_submit_obj %} {% set late = p.check_deadline(round) %} - {% if late %}<span class='sol-late' title="{{ late }}"><b>⚠</b></span> {% endif %} + {% if late %}<span class='sol-warn' title="{{ late }}"><b>⚠</b></span> {% endif %} <a href='{{ paper_link(u, p) }}'> - {{- p.uploaded_at|timeformat }} ({{ p.pages|inflected('strana', 'strany', 'stran') }}) + {{- p.uploaded_at|timeformat }} + {% if p.broken %} + (nekorektní PDF) + {% elif p.pages != None %} + ({{ p.pages|inflected('strana', 'strany', 'stran') }}) + {% endif %} </a> {% if p.uploaded_by_obj != u %} <a href="{{ url_for('org_user', id=p.uploaded_by) }}" title="nahrál {{ p.uploaded_by_obj.full_name() }}" ><b>🛈</b></a> @@ -49,7 +54,12 @@ finální (ve výchozím stavu poslední nahrané).{% endif %} <td>{% if sol.final_feedback_obj %} {% set p = sol.final_feedback_obj %} <a title="nahrál {{ p.uploaded_by_obj.full_name() }}" href='{{ paper_link(u, p) }}'> - {{ (p.uploaded_at if p else None)|timeformat }} ({{ p.pages|inflected('strana', 'strany', 'stran') }}) + {{ (p.uploaded_at if p else None)|timeformat }} + {% if p.broken %} + (nekorektní PDF) + {% elif p.pages != None %} + ({{ p.pages|inflected('strana', 'strany', 'stran') }}) + {% endif %} </a> {% set key = (obj.task_id if for_user else obj.user_id, "feedback") %} {% if key in paper_counts and paper_counts[key] > 1 %} diff --git a/mo/web/templates/user_contest.html b/mo/web/templates/user_contest.html index 49b3f0a0ee64381bbc186905f0aa1b5e9b87b2d6..f7822f71e7edfd608d127ef0c303080c67e8f58a 100644 --- a/mo/web/templates/user_contest.html +++ b/mo/web/templates/user_contest.html @@ -75,7 +75,14 @@ <tr> <td>{{ task.code }}: {{ task.name }} {% if sol.final_submit_obj %} - <td><a href='{{ url_for('user_paper', id=sol.final_submit_obj.paper_id) }}'>{{- sol.final_submit_obj.uploaded_at|timeformat }} ({{ sol.final_submit_obj.pages|inflected('strana', 'strany', 'stran') }})</a> + {% set p = sol.final_submit_obj %} + <td><a href='{{ url_for('user_paper', id=p.paper_id) }}'>{{- p.uploaded_at|timeformat }} + {% if p.broken %} + <strong>(nekorektní PDF)</strong> + {% elif p.pages != None %} + ({{ p.pages|inflected('strana', 'strany', 'stran') }}) + {% endif %} + </a> {% else %} <td> {% endif %} diff --git a/mo/web/templates/user_contest_task.html b/mo/web/templates/user_contest_task.html index 504c4a2d55593ecf4acb5f612236471e1a48ca27..57066df25f26e429c096444b1eb85180e98dfe8b 100644 --- a/mo/web/templates/user_contest_task.html +++ b/mo/web/templates/user_contest_task.html @@ -61,8 +61,8 @@ {% for p in papers %} <tr{% if round.state == RoundState.closed and papers|length > 1 and p.paper_id == sol.final_submit %} class="sol-active"{% endif %}> <td>{{ p.uploaded_at|timeformat }} - <td>{{ p.pages }} - <td>{{ p.bytes }} + <td>{% if p.broken %}nekorektní PDF{% else %}{{ p.pages|or_dash }}{% endif %} + <td>{{ p.bytes|or_dash }} <td>{{ p.uploaded_by_obj.full_name() }} <td>{{ p.note }} <td><a class='btn btn-xs btn-primary' href='{{ url_for('user_paper', id=p.paper_id) }}'>Stáhnout</a> diff --git a/mo/web/user.py b/mo/web/user.py index 9e4bc42f51cc19935939358d723f815c42311062..f4ac0297b3e72e73939b74ce80e9676319ac14bf 100644 --- a/mo/web/user.py +++ b/mo/web/user.py @@ -141,7 +141,12 @@ def user_contest_task(contest_id: int, task_id: int): sess.commit() - flash('Řešení odevzdáno', 'success') + if paper.broken: + flash('Soubor není korektní PDF, ale přesto jsme ho přijali a pokusíme se ho zpracovat. ' + + 'Zkontrolujte prosím, že se na vašem počítači zobrazuje správně.', + 'warning') + else: + flash('Řešení odevzdáno', 'success') return redirect(url_for('user_contest', id=contest_id)) sol = sess.query(db.Solution).filter_by(task=task, user=g.user).one_or_none() diff --git a/static/mo.css b/static/mo.css index 3af005c130d686562f9fb410f334fb1f62513295..4af48c460006ba10768084ab7d2061f7b40ec1aa 100644 --- a/static/mo.css +++ b/static/mo.css @@ -106,7 +106,7 @@ table.data td.sol { table.data td.sol a { color: black; } -table.data td.sol-late { +table.data td.sol-warn { background-color: #ffaaaa; } @@ -180,7 +180,7 @@ nav#main-menu a.active { background-color: yellow; } -.sol-late { +.sol-warn { color: red; }