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

Merge branch 'jirka/export-columns' into 'devel'

Výběr sloupečků v exportech

See merge request mj/mo-submit!115
parents 19cc9e58 7fd93c0e
Branches
No related tags found
1 merge request!115Výběr sloupečků v exportech
......@@ -215,5 +215,10 @@ def org_export_schools():
'kraj_code': k.get_code(),
})
table = Table(school_export_columns, gen_rows(), 'skoly')
table = Table(
id="skoly",
columns=school_export_columns,
rows=gen_rows(),
filename='skoly',
)
return table.send_as(format, streaming=True)
......@@ -551,13 +551,14 @@ def org_generic_list(round_id: Optional[int] = None, hier_id: Optional[int] = No
filter=filter, count=count, action_form=action_form,
)
else:
table = make_contestant_table(query, round, is_export=True)
return table.send_as(format)
table = make_contestant_table(query, round)
return table.send_as(format, args=request.args)
contest_list_columns = (
Column(key='first_name', name='krestni', title='Křestní jméno'),
Column(key='last_name', name='prijmeni', title='Příjmení'),
Column(key='user_id', name='user_id', title='ID uživatele', in_html=False),
Column(key='email', name='email', title='E-mail'),
Column(key='school', name='skola', title='Škola'),
Column(key='school_code', name='kod_skoly', title='Kód školy'),
......@@ -608,7 +609,7 @@ def get_contestants_query(
return query
def make_contestant_table(query: Query, round: db.Round, add_checkbox: bool = False, add_contest_column: bool = False, is_export: bool = False):
def make_contestant_table(query: Query, round: db.Round, add_checkbox: bool = False, add_contest_column: bool = False):
ctants = query.all()
rows: List[Row] = []
......@@ -640,13 +641,12 @@ def make_contestant_table(query: Query, round: db.Round, add_checkbox: bool = Fa
cols: List[Column] = list(contest_list_columns)
if add_checkbox:
cols = [Column(key='checkbox', name=' ', title=' ')] + cols
cols = [Column(key='checkbox', name=' ', title=' ', in_export=None)] + cols
if add_contest_column:
cols.append(Column(key='region_code', name='kod_oblasti', title=round.get_level().name.title()))
if is_export:
cols.append(Column(key='user_id', name='user_id'))
return Table(
id="ucastnici",
columns=cols,
rows=rows,
filename='ucastnici',
......
......
......@@ -141,22 +141,22 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_
subcontest_id_map[(subcontest.round_id, subcontest.master_contest_id)] = subcontest.contest_id
# Construct columns
is_export = (format != "")
columns = []
columns.append(Column(key='order', name='poradi', title='Pořadí'))
if is_export:
columns.append(Column(key='status', name='stav'))
columns.append(Column(key='participant', name='ucastnik', title='Účastník'))
if is_export:
columns.append(Column(key='email', name='email'))
columns = [
Column(key='order', name='poradi', title='Pořadí'),
Column(key='status', name='stav', title='Stav (vítěz, …)', in_html=False),
Column(key='participant', name='ucastnik', title='Účastník', in_export=False),
Column(key='first_name', name='krestni', title='Křestní jméno', in_html=False),
Column(key='last_name', name='prijmeni', title='Příjmení', in_html=False),
Column(key='email', name='email', title='E-mail', in_html=False),
]
if not ct_id:
columns.append(Column(key='contest', name='oblast', title=round.get_level().name.title()))
if is_export:
columns.append(Column(key='pion_place', name='soutezni_misto'))
columns.append(Column(key='school', name='skola', title='Škola'))
columns.append(Column(key='grade', name='rocnik', title='Ročník'))
if is_export:
columns.append(Column(key='birth_year', name='rok_narozeni'))
columns.append(Column(key='contest', name='oblast', title='Soutěžní ' + round.get_level().name))
columns.extend([
Column(key='pion_place', name='soutezni_misto', title='Soutěžní místo', in_html=False),
Column(key='school', name='skola', title='Škola'),
Column(key='grade', name='rocnik', title='Ročník'),
Column(key='birth_year', name='rok_narozeni', title='Rok narození', in_html=False),
])
for task in tasks:
title = task.code
if contest:
......@@ -173,7 +173,7 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_
columns.append(Column(key='total_points', name='celkove_body', title='Celkové body'))
if is_edit:
columns.append(Column(key='suborder', name='zjednoznacneni_poradi', title='Zjednoznačnění'))
# columns.append(Column(key='order_key', name='order_key', title='Třídící klíč'))
columns.append(Column(key='order_key', name='order_key', title='Třídící klíč', in_html=False, in_export=False))
# Construct rows
table_rows = []
......@@ -199,6 +199,8 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_
'order': order_cell,
'status': status,
'user': user,
'first_name': user.first_name,
'last_name': user.last_name,
'email': user.email,
'participant': cell_pion_link(user, local_pion_ct_id, user.full_name()),
'contest': CellLink(pion.contest.place.name or "?", url_for('org_contest', ct_id=pion.contest_id)),
......@@ -228,6 +230,7 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_
if contest:
filename += f"_oblast_{contest.place.code or contest.place.place_id}"
table = Table(
id="vysledky",
table_class="data full center",
columns=columns,
rows=table_rows,
......@@ -254,7 +257,7 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_
edit_form=edit_form, snapshot_form=snapshot_form,
)
else:
return table.send_as(format)
return table.send_as(format, args=request.args)
class SetFinalScoretableForm(FlaskForm):
......@@ -401,11 +404,12 @@ def org_score_snapshot(ct_id: int, scoretable_id: int):
if not scoretable or scoretable.contest_id != ct_id:
raise werkzeug.exceptions.NotFound()
columns, table_rows = scoretable_construct(scoretable, format != "")
columns, table_rows = scoretable_construct(scoretable)
# columns.append(Column(key='order_key', name='order_key', title='Třídící klíč'))
filename = f"vysledky_{ctx.round.year}-{ctx.round.category}-{ctx.round.level}_oblast_{ctx.contest.place.code or ctx.contest.place.place_id}"
table = Table(
id="vysledky",
table_class="data full center",
columns=columns,
rows=table_rows,
......
......
......@@ -4,8 +4,9 @@ from flask import Response, url_for
from html import escape
import io
from markupsafe import Markup
from typing import Any, Dict, Sequence, Optional, Iterable, Union
from typing import Any, Dict, List, Sequence, Optional, Iterable, Union
import urllib.parse
from werkzeug.datastructures import ImmutableMultiDict
import werkzeug.exceptions
from mo.csv import FileFormat
......@@ -19,11 +20,15 @@ class Column:
key: str # Jméno klíče ve slovnících
name: str # Název v hlavičce CSV
title: str # Titulek v HTML tabulkách
in_html: bool # Jestli sloupec zobrazit na webu
in_export: Optional[bool] # Jestli sloupec defaultně exportovat, None = ani nenabízet
def __init__(self, key: str, name: Optional[str] = None, title: Optional[str] = None):
def __init__(self, key: str, name: Optional[str] = None, title: Optional[str] = None, in_html: bool = True, in_export: Optional[bool] = True):
self.key = key
self.name = name or key
self.title = title or self.name
self.in_html = in_html
self.in_export = in_export
class Cell:
......@@ -142,6 +147,7 @@ class OrderCell(Cell):
class Table:
id: str # validní jako HTML id a unikátní pro tabulky zobrazené na stejné stránce
columns: Sequence[Column]
rows: Iterable[Row]
filename: str
......@@ -149,10 +155,11 @@ class Table:
table_class: str
def __init__(
self, columns: Sequence[Column], rows: Iterable[Row],
self, id: str, columns: Sequence[Column], rows: Iterable[Row],
filename: str, show_downlink: bool = True,
table_class: str = "data"
):
self.id = id
self.columns = columns
self.rows = rows
self.filename = filename
......@@ -160,9 +167,11 @@ class Table:
self.table_class = table_class
def to_html(self) -> str:
tab = [f'<table class="{self.table_class}">', '<thead>', '<tr>']
tab = [f'<table class="{self.table_class}" id="{self.id}">', '<thead>', '<tr>']
for c in self.columns:
if not c.in_html:
continue
tab.append(f'\t<th>{c.title}')
tab.append('<tbody>')
......@@ -171,6 +180,8 @@ class Table:
r_attr = [f' {key}="{val}"' for (key, val) in r.html_attr.items()]
tab.append(f'<tr{"".join(r_attr)}>')
for c in self.columns:
if not c.in_html:
continue
val = r.get(c.key)
if isinstance(val, Cell):
tab.append(val.to_html())
......@@ -179,32 +190,56 @@ class Table:
tab.append('</table>')
if self.show_downlink:
tab.append("<p>Stáhnout jako <a href='?format=en_csv'>CSV s čárkami</a>, <a href='?format=cs_csv'>CSV se středníky</a> nebo <a href='?format=tsv'>TSV</a>.")
id = 'column-selection-toggle-' + self.id
tab.extend([
'<form method="GET">',
' <p>Stáhnout jako <button class="btn btn-default btn-xs" name="format" value="en_csv">CSV s čárkami</button>,',
' <button class="btn btn-default btn-xs" name="format" value="cs_csv">CSV se středníky</button> nebo <button class="btn btn-default btn-xs" name="format" value="tsv">TSV</button>.',
f' <div class="collapsible"><input type="checkbox" class="toggle" id="{id}">',
f' <label for="{id}" class="toggle toggle-small">Vybrat sloupce pro stažení</label>',
' <div class="collapsible-inner"><div class="form-frame">',
self.get_columns_checkboxes(line_prefix=" "),
' </div></div>',
' </div>',
'</form>',
])
return Markup("\n".join(tab))
def to_csv(self, fmt: FileFormat) -> bytes:
def get_columns_checkboxes(self, line_prefix: str = "", args: Optional[ImmutableMultiDict] = None) -> Markup:
out = [line_prefix + '<input type="hidden" name="do_column_selection" value="1">']
for c in self.columns:
if c.in_export is None:
continue
checked = c.in_export
if args is not None and args.get('do_column_selection') is not None:
checked = args.get('column_' + c.name) is not None
out.append(f'{line_prefix}<div class="checkbox show"><label><input name="column_{c.name}" type="checkbox"{" checked" * checked} value="1">{c.title} <small>(<code>{c.name}</code></small>)</label></div>')
return Markup("\n".join(out))
def to_csv(self, fmt: FileFormat, export_columns: List[Column]) -> bytes:
out = io.StringIO()
writer = csv.writer(out, dialect=fmt.get_dialect())
header = [c.name for c in self.columns]
header = [c.name for c in export_columns]
writer.writerow(header)
for row in self.rows:
r = [row.get(c.key) for c in self.columns]
r = [row.get(c.key) for c in export_columns]
writer.writerow(r)
return out.getvalue().encode(fmt.get_charset())
def to_csv_stream(self, fmt: FileFormat) -> Iterable[bytes]:
def to_csv_stream(self, fmt: FileFormat, export_columns: List[Column]) -> Iterable[bytes]:
out = io.StringIO()
writer = csv.writer(out, dialect=fmt.get_dialect())
header = [c.name for c in self.columns]
header = [c.name for c in export_columns]
writer.writerow(header)
nrows = 0
for row in self.rows:
r = [row.get(c.key) for c in self.columns]
r = [row.get(c.key) for c in export_columns]
writer.writerow(r)
nrows += 1
......@@ -216,16 +251,23 @@ class Table:
yield out.getvalue().encode(fmt.get_charset())
def send_as(self, format: Union[FileFormat, str], streaming: bool = False) -> Response:
def send_as(self, format: Union[FileFormat, str], streaming: bool = False, args: Optional[ImmutableMultiDict] = None) -> Response:
try:
fmt = FileFormat.coerce(format)
except ValueError:
raise werkzeug.exceptions.NotFound()
export_columns = [c for c in self.columns if c.in_export]
if args is not None and args.get('do_column_selection') is not None:
export_columns = [c for c in self.columns if args.get('column_' + c.name) is not None]
if len(export_columns) == 0:
raise werkzeug.exceptions.BadRequest("Žádné sloupce v exportu, musíte zvolit alespoň jeden")
if streaming:
resp = Response(self.to_csv_stream(fmt))
resp = Response(self.to_csv_stream(fmt, export_columns))
else:
out = self.to_csv(fmt)
out = self.to_csv(fmt, export_columns)
resp = app.make_response(out)
resp.content_type = fmt.get_content_type()
......
......
......@@ -63,6 +63,14 @@
Celkem <b>{{count|inflected('nalezený účastník', 'nalezení účastníci', 'nalezených účastníků')}}</b>.
{% endif %}
</div>
<div class="form-row">
<div class="collapsible"><input type="checkbox" class="toggle" id="column-selection-toggle">
<label for="column-selection-toggle" class="toggle toggle-small">Vybrat sloupce pro stažení</label>
<div class="collapsible-inner"><div class="form-frame">
{{ table.get_columns_checkboxes(line_prefix="\t\t\t\t", args=request.args) }}
</div></div>
</div>
</div>
</form>
</div>
......
......
......@@ -38,7 +38,7 @@
{% elif type == "warning" %}<li>Varování: {{ msg }}
{% else %}<li class="text-info">Info: {{ msg }}{% endif %}
{% endfor %}
</ul></div>
</ul>
</div>
</div>
</div>
......
......
......@@ -495,18 +495,17 @@ def user_paper(contest_id: int, paper_id: int):
return mo.web.util.send_task_paper(paper)
def scoretable_construct(scoretable: db.ScoreTable, is_export: bool = False) -> Tuple[List[Column], List[Row]]:
def scoretable_construct(scoretable: db.ScoreTable) -> Tuple[List[Column], List[Row]]:
"""Pro konstrukci výsledkovky zobrazované soutěžícím. Využito i při zobrazení
uložených snapshotů výsledkovky v org_score.py.
"""
columns = [
Column(key='order', name='poradi', title='Pořadí'),
Column(key='status', name='stav', title='Stav účasti', in_html=False),
Column(key='name', name='ucastnik', title='Účastník'),
Column(key='school', name='skola', title='Škola'),
Column(key='grade', name='rocnik', title='Ročník')
Column(key='grade', name='rocnik', title='Ročník'),
]
if is_export:
columns.insert(1, Column(key='status', name='stav'))
for (code, name) in scoretable.tasks:
columns.append(Column(key=f'task_{code}', name=code, title=code))
......@@ -544,11 +543,12 @@ def user_contest_score(id: int):
if not contest.scoretable or state not in [db.RoundState.graded, db.RoundState.closed]:
raise werkzeug.exceptions.NotFound()
columns, table_rows = scoretable_construct(contest.scoretable, format != "")
columns, table_rows = scoretable_construct(contest.scoretable)
# columns.append(Column(key='order_key', name='order_key', title='Třídící klíč'))
filename = f"vysledky_{round.year}-{round.category}-{round.level}_oblast_{contest.place.code or contest.place.place_id}"
table = Table(
id="vysledky",
table_class="data full center",
columns=columns,
rows=table_rows,
......
......
......@@ -388,6 +388,10 @@ div.alert + div.alert {
cursor: pointer;
margin-left: 15px;
}
.collapsible label.toggle-small {
font-weight: normal;
margin-bottom: 0px;
}
.collapsible label.toggle::before {
position: absolute;
content: "";
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment