Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • devel
  • fo
  • fo-base
  • honza/add-contestant
  • honza/kolo-vs-soutez
  • honza/mr6
  • honza/mr7
  • honza/mra
  • honza/mrd
  • honza/mrf
  • honza/submit-images
  • jh-stress-test-wip
  • jirka/typing
  • jk/issue-196
  • jk/issue-96
  • master
  • mj/submit-images
  • shorten-schools
18 results

Target

Select target project
  • mj/mo-submit
1 result
Select Git revision
  • devel
  • fo
  • fo-base
  • honza/add-contestant
  • honza/kolo-vs-soutez
  • honza/mr6
  • honza/mr7
  • honza/mra
  • honza/mrd
  • honza/mrf
  • honza/submit-images
  • jh-stress-test-wip
  • jirka/typing
  • jk/issue-196
  • jk/issue-96
  • master
  • mj/submit-images
  • shorten-schools
18 results
Show changes
Commits on Source (16)
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
# - quoting pomocí uvozovek # - quoting pomocí uvozovek
import csv import csv
import difflib
from enum import auto from enum import auto
from dataclasses import dataclass, fields from dataclasses import dataclass, fields
from typing import Type, List, IO, Sequence from typing import Type, List, IO, Sequence
...@@ -108,6 +109,7 @@ def write(file: IO, fmt: FileFormat, row_class: Type[Row], rows: Sequence[Row]): ...@@ -108,6 +109,7 @@ def write(file: IO, fmt: FileFormat, row_class: Type[Row], rows: Sequence[Row]):
def read(file: IO, fmt: FileFormat, row_class: Type[Row]): def read(file: IO, fmt: FileFormat, row_class: Type[Row]):
reader = csv.reader(file, dialect=fmt.get_dialect(), strict=True) reader = csv.reader(file, dialect=fmt.get_dialect(), strict=True)
warnings = []
header: List[str] = [] header: List[str] = []
rows: List[Row] = [] rows: List[Row] = []
columns = set(field.name for field in fields(row_class)) columns = set(field.name for field in fields(row_class))
...@@ -120,6 +122,13 @@ def read(file: IO, fmt: FileFormat, row_class: Type[Row]): ...@@ -120,6 +122,13 @@ def read(file: IO, fmt: FileFormat, row_class: Type[Row]):
header = r header = r
if not any(h in columns for h in header): if not any(h in columns for h in header):
raise MissingHeaderError() raise MissingHeaderError()
for h in header:
if not h in columns:
best_matches = difflib.get_close_matches(h, columns, n=1, cutoff=0.8)
if best_matches:
warnings.append(
"Neznámý sloupec '{}', měli jste na mysli '{}'?".format(
h, best_matches[0]))
else: else:
row = row_class() row = row_class()
not_empty = False not_empty = False
...@@ -133,4 +142,4 @@ def read(file: IO, fmt: FileFormat, row_class: Type[Row]): ...@@ -133,4 +142,4 @@ def read(file: IO, fmt: FileFormat, row_class: Type[Row]):
if not_empty: if not_empty:
rows.append(row) rows.append(row)
return rows return (rows, warnings)
...@@ -118,15 +118,8 @@ class Place(Base): ...@@ -118,15 +118,8 @@ class Place(Base):
return len(PlaceType.choices(level=self.level + 1)) > 0 return len(PlaceType.choices(level=self.level + 1)) > 0
# Předpokládáme, že za běhu aplikace se root nezmění
root_place_cache: Optional[Place] = None
def get_root_place(): def get_root_place():
global root_place_cache return get_session().query(Place).filter_by(parent=None).one()
if root_place_cache is None:
root_place_cache = get_session().query(Place).filter_by(parent=None).one()
return root_place_cache
def get_place_by_code(code: str, fetch_school: bool = False) -> Optional[Place]: def get_place_by_code(code: str, fetch_school: bool = False) -> Optional[Place]:
......
...@@ -36,6 +36,7 @@ import_type_names = { ...@@ -36,6 +36,7 @@ import_type_names = {
class Import: class Import:
# Výsledek importu # Výsledek importu
errors: List[str] errors: List[str]
warnings: List[str]
cnt_rows: int = 0 cnt_rows: int = 0
cnt_new_users: int = 0 cnt_new_users: int = 0
cnt_new_participants: int = 0 cnt_new_participants: int = 0
...@@ -69,6 +70,7 @@ class Import: ...@@ -69,6 +70,7 @@ class Import:
def __init__(self): def __init__(self):
self.errors = [] self.errors = []
self.warnings = []
self.rr = None self.rr = None
self.place_cache = {} self.place_cache = {}
self.school_place_cache = {} self.school_place_cache = {}
...@@ -164,11 +166,16 @@ class Import: ...@@ -164,11 +166,16 @@ class Import:
# lidé připisují všechny možné i nemožné znaky, které vypadají jako apostrof :) # lidé připisují všechny možné i nemožné znaky, které vypadají jako apostrof :)
rocnik = re.sub('[\'"\u00b4\u2019]', "", rocnik) rocnik = re.sub('[\'"\u00b4\u2019]', "", rocnik)
if (school.is_ss and re.fullmatch(r'\d/\d', rocnik) if (not re.fullmatch(r'\d(/\d)?', rocnik)):
or school.is_zs and re.fullmatch(r'\d', rocnik)): return self.error(f'Ročník má neplatný formát, musí to být buď číslice, nebo číslice/číslice')
return rocnik
if (not school.is_zs and re.fullmatch(r'\d', rocnik)):
return self.error(f'Ročník pro střední školu ({school.place.name}) zapisujte ve formátu číslice/číslice')
return self.error('Ročník neodpovídá typu školy: pro základní je to číslice, pro střední číslice/číslice') if (not school.is_ss and re.fullmatch(r'\d/\d', rocnik)):
return self.error(f'Ročník pro základní školu ({school.place.name}) zapisujte jako číslici 1–9')
return rocnik
def parse_born(self, rok: str) -> Optional[int]: def parse_born(self, rok: str) -> Optional[int]:
if not re.fullmatch(r'\d{4}', rok): if not re.fullmatch(r'\d{4}', rok):
...@@ -377,9 +384,11 @@ class Import: ...@@ -377,9 +384,11 @@ class Import:
try: try:
with open(path, encoding=charset) as file: with open(path, encoding=charset) as file:
try: try:
rows: List[mo.csv.Row] = mo.csv.read(file=file, fmt=self.fmt, row_class=self.row_class) rows: List[mo.csv.Row]
rows, warnings = mo.csv.read(file=file, fmt=self.fmt, row_class=self.row_class)
self.warnings += warnings
except MissingHeaderError: except MissingHeaderError:
return self.error('Souboru chybí hlavička s názvy sloupců') return self.error('Souboru chybí první řádek s názvy sloupců')
except UnicodeDecodeError: except UnicodeDecodeError:
return self.error(f'Soubor není v kódování {self.fmt.get_charset()}') return self.error(f'Soubor není v kódování {self.fmt.get_charset()}')
except Exception as e: except Exception as e:
...@@ -563,9 +572,11 @@ class JudgeImport(Import): ...@@ -563,9 +572,11 @@ class JudgeImport(Import):
log_msg_prefix = 'Opravovatelé' log_msg_prefix = 'Opravovatelé'
log_details = {'action': 'import-judges'} log_details = {'action': 'import-judges'}
template_basename = 'sablona-oprav' template_basename = 'sablona-oprav'
root_place: db.Place
def setup(self): def setup(self):
assert self.round is not None assert self.round is not None
self.root_place = db.get_root_place()
def import_row(self, r: mo.csv.Row): def import_row(self, r: mo.csv.Row):
assert isinstance(r, JudgeImportRow) assert isinstance(r, JudgeImportRow)
...@@ -586,7 +597,7 @@ class JudgeImport(Import): ...@@ -586,7 +597,7 @@ class JudgeImport(Import):
return return
contest = self.obtain_contest(oblast, allow_none=True) contest = self.obtain_contest(oblast, allow_none=True)
place = contest.place if contest else db.get_root_place() place = contest.place if contest else self.root_place
if not self.check_rights(place): if not self.check_rights(place):
return self.error(f'K místu "{place.get_code()}" nemáte práva na správu soutěže') return self.error(f'K místu "{place.get_code()}" nemáte práva na správu soutěže')
......
...@@ -115,7 +115,7 @@ def send_new_account_email(user: db.User, token: str) -> bool: ...@@ -115,7 +115,7 @@ def send_new_account_email(user: db.User, token: str) -> bool:
return send_user_email(user, 'Založen nový účet', textwrap.dedent('''\ return send_user_email(user, 'Založen nový účet', textwrap.dedent('''\
Vítejte! Vítejte!
Právě Vám byl založen účet v Odevzávacím systému Matematické olympiády. Právě Vám byl založen účet v Odevzdávacím systému Matematické olympiády.
Nastavte si prosím heslo na následující stránce: Nastavte si prosím heslo na následující stránce:
{} {}
...@@ -126,7 +126,7 @@ def send_new_account_email(user: db.User, token: str) -> bool: ...@@ -126,7 +126,7 @@ def send_new_account_email(user: db.User, token: str) -> bool:
def send_password_reset_email(user: db.User, token: str) -> bool: def send_password_reset_email(user: db.User, token: str) -> bool:
return send_user_email(user, 'Obnova hesla', textwrap.dedent('''\ return send_user_email(user, 'Obnova hesla', textwrap.dedent('''\
Někdo (pravděpodobně Vy) požádal o obnovení hesla k Vašemu účtu v Odevzávacím Někdo (pravděpodobně Vy) požádal o obnovení hesla k Vašemu účtu v Odevzdávacím
systému Matematické olympiády. Heslo si můžete nastavit, případně požadavek systému Matematické olympiády. Heslo si můžete nastavit, případně požadavek
zrušit, na následující stránce: zrušit, na následující stránce:
......
...@@ -364,6 +364,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]): ...@@ -364,6 +364,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]):
form = ImportForm() form = ImportForm()
errs = [] errs = []
warnings = []
if form.validate_on_submit(): if form.validate_on_submit():
fmt = form.fmt.data fmt = form.fmt.data
imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=round, contest=contest) imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=round, contest=contest)
...@@ -383,6 +384,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]): ...@@ -383,6 +384,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]):
return redirect(url_for('org_round', id=round.round_id)) return redirect(url_for('org_round', id=round.round_id))
else: else:
errs = imp.errors errs = imp.errors
warnings = imp.warnings
else: else:
flash('Vyberte si prosím soubor', 'danger') flash('Vyberte si prosím soubor', 'danger')
elif form.get_template.data: elif form.get_template.data:
...@@ -398,6 +400,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]): ...@@ -398,6 +400,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]):
round=round, round=round,
form=form, form=form,
errs=errs, errs=errs,
warnings=warnings
) )
...@@ -1283,6 +1286,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d ...@@ -1283,6 +1286,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
form = BatchPointsForm() form = BatchPointsForm()
errs = [] errs = []
warnings = []
if form.validate_on_submit(): if form.validate_on_submit():
fmt = form.fmt.data fmt = form.fmt.data
imp = create_import(user=g.user, type=ImportType.points, fmt=fmt, round=round, contest=contest, task=task, allow_add_del=form.add_del_sols.data) imp = create_import(user=g.user, type=ImportType.points, fmt=fmt, round=round, contest=contest, task=task, allow_add_del=form.add_del_sols.data)
...@@ -1302,6 +1306,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d ...@@ -1302,6 +1306,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
return redirect(url_for('org_round', id=round.round_id)) return redirect(url_for('org_round', id=round.round_id))
else: else:
errs = imp.errors errs = imp.errors
warnings = imp.warnings
else: else:
flash('Vyberte si prosím soubor', 'danger') flash('Vyberte si prosím soubor', 'danger')
elif form.get_template.data: elif form.get_template.data:
...@@ -1316,6 +1321,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d ...@@ -1316,6 +1321,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
round=round, contest=contest, task=task, round=round, contest=contest, task=task,
form=form, form=form,
errs=errs, errs=errs,
warnings=warnings
) )
......
...@@ -346,6 +346,11 @@ class MODateTimeField(wtforms.DateTimeField): ...@@ -346,6 +346,11 @@ class MODateTimeField(wtforms.DateTimeField):
def __init__(self, label, format='%Y-%m-%d %H:%M', description='Ve formátu 2000-01-01 12:34', **kwargs): def __init__(self, label, format='%Y-%m-%d %H:%M', description='Ve formátu 2000-01-01 12:34', **kwargs):
super().__init__(label, format=format, description=description, **kwargs) super().__init__(label, format=format, description=description, **kwargs)
def process_data(self, valuelist):
super().process_formdata(valuelist)
if self.data is not None:
self.data = self.data.astimezone()
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
super().process_formdata(valuelist) super().process_formdata(valuelist)
if self.data is not None: if self.data is not None:
......
...@@ -8,14 +8,24 @@ ...@@ -8,14 +8,24 @@
{% block body %} {% block body %}
{% if warnings %}
<h3>Varování při importu</h3>
<div class="alert alert-warning" role="alert" style="white-space: pre-line">{{ "" -}}
{% for e in warnings %}
{{ e }}
{% endfor %}
</div>
{% endif %}
{% if errs %} {% if errs %}
<h3>Chyby při importu</h3> <h3>Chyby při importu</h3>
<pre><div class="alert alert-danger" role="alert">{{ "" -}} <div class="alert alert-danger" role="alert" style="white-space: pre-line">{{ "" -}}
{% for e in errs %} {% for e in errs %}
{{ e }} {{ e }}
{% endfor %} {% endfor %}
</div></pre> </div>
{% endif %} {% endif %}
<p>Zde si můžete stáhnout bodovací formulář v zadaném formátu a pak ho nahrát zpět <p>Zde si můžete stáhnout bodovací formulář v zadaném formátu a pak ho nahrát zpět
......
...@@ -9,14 +9,24 @@ Import dat do {% if contest %}soutěžní oblasti {{ contest.place.name }}{% els ...@@ -9,14 +9,24 @@ Import dat do {% if contest %}soutěžní oblasti {{ contest.place.name }}{% els
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% if warnings %}
<h3>Varování při importu</h3>
<div class="alert alert-warning" role="alert" style="white-space: pre-line">{{ "" -}}
{% for e in warnings %}
{{ e }}
{% endfor %}
</div>
{% endif %}
{% if errs %} {% if errs %}
<h3>Chyby při importu</h3> <h3>Chyby při importu</h3>
<pre><div class="alert alert-danger" role="alert">{{ "" -}} <div class="alert alert-danger" role="alert" style="white-space: pre-line">{{ "" -}}
{% for e in errs %} {% for e in errs %}
{{ e }} {{ e }}
{% endfor %} {% endfor %}
</div></pre> </div>
{% endif %} {% endif %}
<p>Zde je možné importovat účastníky soutěže, dozor na soutěžních místech a opravovatele. <p>Zde je možné importovat účastníky soutěže, dozor na soutěžních místech a opravovatele.
......
...@@ -24,11 +24,11 @@ ...@@ -24,11 +24,11 @@
{% if has_errors %} {% if has_errors %}
<h3>Chyby</h3> <h3>Chyby</h3>
<pre><div class="alert alert-danger" role="alert">{{ "" -}} <div class="alert alert-danger" role="alert" style="white-space: pre-line">{{ "" -}}
{% for e in job.out_json['errors'] %} {% for e in job.out_json['errors'] %}
{{ e }} {{ e }}
{% endfor %} {% endfor %}
</div></pre> </div>
{% endif %} {% endif %}
......
...@@ -104,11 +104,13 @@ ...@@ -104,11 +104,13 @@
{% if can_manage_round %} {% if can_manage_round %}
<td><div class="btn-group"> <td><div class="btn-group">
<a class="btn btn-xs btn-primary" href="{{ url_for('org_round_task_edit', id=round.round_id, task_id=task.task_id) }}">Editovat</a> <a class="btn btn-xs btn-primary" href="{{ url_for('org_round_task_edit', id=round.round_id, task_id=task.task_id) }}">Editovat</a>
{% if task.sol_count == 0 %}
<form action="" method="POST" onsubmit="return confirm('Opravdu nenávratně smazat?')" class="btn-group"> <form action="" method="POST" onsubmit="return confirm('Opravdu nenávratně smazat?')" class="btn-group">
{{ form_delete_task.csrf_token() }} {{ form_delete_task.csrf_token() }}
<input type="hidden" name="delete_task_id" value="{{ task.task_id }}"> <input type="hidden" name="delete_task_id" value="{{ task.task_id }}">
<button type="submit" class="btn btn-xs btn-danger">Smazat</button> <button type="submit" class="btn btn-xs btn-danger">Smazat</button>
</form> </form>
{% endif %}
{% if g.user.is_admin %} {% if g.user.is_admin %}
<a class="btn btn-xs btn-default" href="{{ log_url('task', task.task_id) }}">Historie</a> <a class="btn btn-xs btn-default" href="{{ log_url('task', task.task_id) }}">Historie</a>
{% endif %} {% endif %}
......
...@@ -66,7 +66,7 @@ Existuje více než jedna verze řešení, finální je podbarvená. ...@@ -66,7 +66,7 @@ Existuje více než jedna verze řešení, finální je podbarvená.
<td{% if late %} class='sol-warn'{% endif %}>{{ p.uploaded_at|timeformat }} <td{% if late %} class='sol-warn'{% endif %}>{{ p.uploaded_at|timeformat }}
<td>{% if p.broken %}nekorektní PDF{% else %}{{ p.pages|or_dash }}{% endif %} <td>{% if p.broken %}nekorektní PDF{% else %}{{ p.pages|or_dash }}{% endif %}
<td>{{ p.bytes|or_dash }} <td>{{ p.bytes|or_dash }}
<td>{{ p.uploaded_by_obj|user_link }} <td>{% if p.uploaded_by_obj == sc.user %}<i>účastník</i>{% else %}{{ p.uploaded_by_obj|user_link }}{% endif %}
<td>{% if late %}<span class='sol-warn'>({{ late }})</span> {% endif %}{{ p.note }} <td>{% if late %}<span class='sol-warn'>({{ late }})</span> {% endif %}{{ p.note }}
<td><div class="btn-group"> <td><div class="btn-group">
<a class='btn btn-xs btn-primary' href='{{ paper_link(p) }}'>Stáhnout</a> <a class='btn btn-xs btn-primary' href='{{ paper_link(p) }}'>Stáhnout</a>
...@@ -161,10 +161,10 @@ Existuje více než jedna verze oprav, finální je podbarvená. ...@@ -161,10 +161,10 @@ Existuje více než jedna verze oprav, finální je podbarvená.
</div> </div>
{% else %} {% else %}
<p>Žádné odevzdané řešení. {% if form %}Můžete ho založit pomocí formuláře níže.{% endif %} <p>Žádné odevzdané řešení. {% if form and sc.allow_edit_points %}Můžete ho založit pomocí formuláře níže.{% endif %}
{% endif %} {% endif %}
{% if form %} {% if form and (sc.allow_edit_points or sc.allow_upload_feedback or sc.allow_upload_solutions) %}
<form method="post" class="form-horizontal" enctype="multipart/form-data"> <form method="post" class="form-horizontal" enctype="multipart/form-data">
<div class="form-frame"> <div class="form-frame">
{{ form.csrf_token }} {{ form.csrf_token }}
......
...@@ -46,5 +46,7 @@ ...@@ -46,5 +46,7 @@
</form> </form>
{% else %} {% else %}
{{ table.to_html() }} {{ table.to_html() }}
<p>
<i>Nemáte právo k editaci účastníků v této oblasti.</i> <i>Nemáte právo k editaci účastníků v této oblasti.</i>
</p>
{% endif %} {% endif %}