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

Target

Select target project
  • mj/mo-submit
1 result
Select Git revision
Show changes
Commits on Source (16)
......@@ -7,6 +7,7 @@
# - quoting pomocí uvozovek
import csv
import difflib
from enum import auto
from dataclasses import dataclass, fields
from typing import Type, List, IO, Sequence
......@@ -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]):
reader = csv.reader(file, dialect=fmt.get_dialect(), strict=True)
warnings = []
header: List[str] = []
rows: List[Row] = []
columns = set(field.name for field in fields(row_class))
......@@ -120,6 +122,13 @@ def read(file: IO, fmt: FileFormat, row_class: Type[Row]):
header = r
if not any(h in columns for h in header):
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:
row = row_class()
not_empty = False
......@@ -133,4 +142,4 @@ def read(file: IO, fmt: FileFormat, row_class: Type[Row]):
if not_empty:
rows.append(row)
return rows
return (rows, warnings)
......@@ -118,15 +118,8 @@ class Place(Base):
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():
global root_place_cache
if root_place_cache is None:
root_place_cache = get_session().query(Place).filter_by(parent=None).one()
return root_place_cache
return get_session().query(Place).filter_by(parent=None).one()
def get_place_by_code(code: str, fetch_school: bool = False) -> Optional[Place]:
......
......@@ -36,6 +36,7 @@ import_type_names = {
class Import:
# Výsledek importu
errors: List[str]
warnings: List[str]
cnt_rows: int = 0
cnt_new_users: int = 0
cnt_new_participants: int = 0
......@@ -69,6 +70,7 @@ class Import:
def __init__(self):
self.errors = []
self.warnings = []
self.rr = None
self.place_cache = {}
self.school_place_cache = {}
......@@ -164,11 +166,16 @@ class Import:
# lidé připisují všechny možné i nemožné znaky, které vypadají jako apostrof :)
rocnik = re.sub('[\'"\u00b4\u2019]', "", rocnik)
if (school.is_ss and re.fullmatch(r'\d/\d', rocnik)
or school.is_zs and re.fullmatch(r'\d', rocnik)):
return rocnik
if (not re.fullmatch(r'\d(/\d)?', rocnik)):
return self.error(f'Ročník má neplatný formát, musí to být buď číslice, nebo číslice/číslice')
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]:
if not re.fullmatch(r'\d{4}', rok):
......@@ -377,9 +384,11 @@ class Import:
try:
with open(path, encoding=charset) as file:
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:
return self.error('Souboru chybí hlavička s názvy sloupců')
return self.error('Souboru chybí první řádek s názvy sloupců')
except UnicodeDecodeError:
return self.error(f'Soubor není v kódování {self.fmt.get_charset()}')
except Exception as e:
......@@ -563,9 +572,11 @@ class JudgeImport(Import):
log_msg_prefix = 'Opravovatelé'
log_details = {'action': 'import-judges'}
template_basename = 'sablona-oprav'
root_place: db.Place
def setup(self):
assert self.round is not None
self.root_place = db.get_root_place()
def import_row(self, r: mo.csv.Row):
assert isinstance(r, JudgeImportRow)
......@@ -586,7 +597,7 @@ class JudgeImport(Import):
return
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):
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:
return send_user_email(user, 'Založen nový účet', textwrap.dedent('''\
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:
{}
......@@ -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:
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
zrušit, na následující stránce:
......
......@@ -364,6 +364,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]):
form = ImportForm()
errs = []
warnings = []
if form.validate_on_submit():
fmt = form.fmt.data
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]):
return redirect(url_for('org_round', id=round.round_id))
else:
errs = imp.errors
warnings = imp.warnings
else:
flash('Vyberte si prosím soubor', 'danger')
elif form.get_template.data:
......@@ -398,6 +400,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]):
round=round,
form=form,
errs=errs,
warnings=warnings
)
......@@ -1283,6 +1286,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
form = BatchPointsForm()
errs = []
warnings = []
if form.validate_on_submit():
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)
......@@ -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))
else:
errs = imp.errors
warnings = imp.warnings
else:
flash('Vyberte si prosím soubor', 'danger')
elif form.get_template.data:
......@@ -1316,6 +1321,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
round=round, contest=contest, task=task,
form=form,
errs=errs,
warnings=warnings
)
......
......@@ -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):
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):
super().process_formdata(valuelist)
if self.data is not None:
......
......@@ -8,14 +8,24 @@
{% 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 %}
<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 %}
{{ e }}
{% endfor %}
</div></pre>
</div>
{% endif %}
<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
{% endblock %}
{% 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 %}
<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 %}
{{ e }}
{% endfor %}
</div></pre>
</div>
{% endif %}
<p>Zde je možné importovat účastníky soutěže, dozor na soutěžních místech a opravovatele.
......
......@@ -24,11 +24,11 @@
{% if has_errors %}
<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'] %}
{{ e }}
{% endfor %}
</div></pre>
</div>
{% endif %}
......
......@@ -104,11 +104,13 @@
{% if can_manage_round %}
<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>
{% if task.sol_count == 0 %}
<form action="" method="POST" onsubmit="return confirm('Opravdu nenávratně smazat?')" class="btn-group">
{{ form_delete_task.csrf_token() }}
<input type="hidden" name="delete_task_id" value="{{ task.task_id }}">
<button type="submit" class="btn btn-xs btn-danger">Smazat</button>
</form>
{% endif %}
{% if g.user.is_admin %}
<a class="btn btn-xs btn-default" href="{{ log_url('task', task.task_id) }}">Historie</a>
{% endif %}
......
......@@ -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 p.broken %}nekorektní PDF{% else %}{{ p.pages|or_dash }}{% endif %}
<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><div class="btn-group">
<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á.
</div>
{% 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 %}
{% 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">
<div class="form-frame">
{{ form.csrf_token }}
......
......@@ -46,5 +46,7 @@
</form>
{% else %}
{{ table.to_html() }}
<p>
<i>Nemáte právo k editaci účastníků v této oblasti.</i>
</p>
{% endif %}