From ad91ee1225b7d0ddf819d9a82426d07e4adb7cc0 Mon Sep 17 00:00:00 2001 From: Martin Mares <mj@ucw.cz> Date: Mon, 28 Aug 2023 15:12:53 +0200 Subject: [PATCH] =?UTF-8?q?Tabulka=20v=C5=A1ech=20=C5=99e=C5=A1en=C3=AD=20?= =?UTF-8?q?v=20sout=C4=9B=C5=BEi=20u=C5=BE=20tak=C3=A9=20um=C3=AD=20editov?= =?UTF-8?q?at=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sdílíme velkou část kódu s tabulkou řešení úlohy. --- mo/web/org_contest.py | 254 +++++++++--------- mo/web/templates/org_contest_solutions.html | 76 ++++-- .../templates/parts/org_solution_table.html | 5 +- 3 files changed, 190 insertions(+), 145 deletions(-) diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index c0c9ca82..d5c6ca3a 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from decimal import Decimal import os from flask import render_template, g, redirect, url_for, flash, request @@ -1053,8 +1054,99 @@ def get_solutions_query( return query +@dataclass +class SolEdit: + key: str + pion: db.Participation + task: db.Task + sol: Optional[db.Solution] + gp: Optional[GenPoints] = None + + +def process_solution_edits( + ctx: Context, + edits: List[SolEdit], + edit_errors: Dict[str, str], +) -> bool: + """Společná implementace editování zobecněných bodů pro org_contest_task a org_contest_solutions.""" + + # Parsování bodů + for edit in edits: + pts = request.form.get(f"points_{edit.key}") + gp = GenPoints.parse(pts) + if gp.error is not None: + edit_errors[edit.key] = gp.error + edit.gp = gp + + if edit_errors: + return False + + cnt_new_sols: int = 0 + cnt_deleted_sols: int = 0 + cnt_changed_sols: int = 0 + to_log: List[str] = [] + + def edit_sol(edit) -> Optional[str]: + try: + sact = SolAction( + task=edit.task, + user=edit.pion.user, + sol=edit.sol, + gp=edit.gp, + reason='web', + rights=ctx.rights, + to_log=to_log, + ) + + added, deleted = sact.add_or_del() + if added: + nonlocal cnt_new_sols + cnt_new_sols += 1 + if deleted: + nonlocal cnt_deleted_sols + cnt_deleted_sols += 1 + + if sact.set_points(): + nonlocal cnt_changed_sols + cnt_changed_sols += 1 + + sact.log_changes() + + except SolActionError as e: + return str(e) + + return None + + # Připravíme všechny změny, ale ještě necommitujeme ani nelogujeme + for edit in edits: + err = edit_sol(edit) + if err is not None: + edit_errors[edit.key] = err + + if edit_errors: + return False + + # Pokud jsme nic neudělali, vrátíme se + if cnt_new_sols + cnt_deleted_sols + cnt_changed_sols == 0: + flash("Žádné změny k uložení.", "info") + return True + + # Teď už víme, že se všechno povede, takže log a commit + for msg in to_log: + app.logger.info(msg) + if cnt_new_sols: + flash(inflect_with_number(cnt_new_sols, "Založeno %s nové řešení.", "Založena %s nová řešení.", "Založeno %s nových řešení."), "success") + if cnt_deleted_sols: + flash(inflect_with_number(cnt_deleted_sols, "Smazáno %s řešení.", "Smazána %s řešení.", "Smazáno %s nových řešení."), "success") + if cnt_changed_sols: + flash("Změněny body u " + inflect_number(cnt_changed_sols, "řešení", "řešení", "řešení") + '.', "success") + sess = db.get_session() + sess.commit() + return True + + class TaskPointsEditForm(FlaskForm): - submit = wtforms.SubmitField("Uložit") + submit = wtforms.SubmitField("Uložit změny") @app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/') @@ -1076,93 +1168,23 @@ def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None): rows.sort(key=lambda r: r[0].user.sort_key()) edit_form: Optional[TaskPointsEditForm] = None - edit_errors: Dict[int, str] = {} - - def process_edit_form() -> bool: - assert edit_form is not None - if not edit_form.validate_on_submit(): - return False - - # Parsování bodů - gen_pts_for: Dict[db.Participation, GenPoints] = {} - for pion, sol in rows: - pts = request.form.get(f"points_{pion.user_id}") - gp = GenPoints.parse(pts) - if gp.error is not None: - edit_errors[pion.user_id] = gp.error - gen_pts_for[pion] = gp - - if edit_errors: - return False - - cnt_new_sols: int = 0 - cnt_deleted_sols: int = 0 - cnt_changed_sols: int = 0 - to_log: List[str] = [] - - def update_sol(pion, sol, gp) -> Optional[str]: - try: - sact = SolAction( - task=assert_not_none(ctx.task), - user=pion.user, - sol=sol, - gp=gp, - reason='web', - rights=ctx.rights, - to_log=to_log, - ) - - added, deleted = sact.add_or_del() - if added: - nonlocal cnt_new_sols - cnt_new_sols += 1 - if deleted: - nonlocal cnt_deleted_sols - cnt_deleted_sols += 1 - - if sact.set_points(): - nonlocal cnt_changed_sols - cnt_changed_sols += 1 - - sact.log_changes() - - except SolActionError as e: - return str(e) - - return None - - # Připravíme všechny změny, ale ještě necommitujeme ani nelogujeme - for pion, sol in rows: - err = update_sol(pion, sol, gen_pts_for[pion]) - if err is not None: - edit_errors[pion.user_id] = err - - if edit_errors: - return False - - # Pokud jsme nic neudělali, vrátíme se - if cnt_new_sols + cnt_deleted_sols + cnt_changed_sols == 0: - flash("Žádné změny k uložení.", "info") - return True - - # Teď už víme, že se všechno povede, takže log a commit - for msg in to_log: - app.logger.info(msg) - if cnt_new_sols: - flash(inflect_with_number(cnt_new_sols, "Založeno %s nové řešení.", "Založena %s nová řešení.", "Založeno %s nových řešení."), "success") - if cnt_deleted_sols: - flash(inflect_with_number(cnt_deleted_sols, "Smazáno %s řešení.", "Smazána %s řešení.", "Smazáno %s nových řešení."), "success") - if cnt_changed_sols: - flash("Změněny body u " + inflect_number(cnt_changed_sols, "řešení", "řešení", "řešení") + '.', "success") - sess.commit() - return True + edit_errors: Dict[str, str] = {} if action_edit: edit_form = TaskPointsEditForm() - if process_edit_form(): - return redirect(ctx.url_for('org_contest_task')) - if edit_errors: - flash("Některé změny se nepodařilo provést. Důvod uvidíte po najetí myší nad políčka s červeným okrajem.", "danger") + if edit_form.validate_on_submit(): + edits = [ + SolEdit( + key=str(pion.user_id), + pion=pion, + task=assert_not_none(ctx.task), + sol=sol, + ) for pion, sol in rows + ] + if process_solution_edits(ctx, edits, edit_errors): + return redirect(ctx.url_for('org_contest_task')) + if edit_errors: + flash("Některé změny se nepodařilo provést. Důvod uvidíte po najetí myší nad políčka s červeným okrajem.", "danger") # Count papers for each solution paper_counts = {} @@ -1186,7 +1208,7 @@ def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None): class ContestSolutionsEditForm(FlaskForm): - submit = wtforms.SubmitField("Založit označená řešení") + submit = wtforms.SubmitField("Uložit změny") @app.route('/org/contest/c/<int:ct_id>/solutions', methods=('GET', 'POST')) @@ -1227,16 +1249,6 @@ def org_contest_solutions(ct_id: int, site_id: Optional[int] = None): joinedload(db.Solution.final_feedback_obj) ).all() - # Count papers for each task and solution - paper_counts = {} - for user_id, task_id, type, count in ( - db.get_session().query(db.Paper.for_user, db.Paper.for_task, db.Paper.type, func.count(db.Paper.type)) - .filter(db.Paper.for_task.in_(tasks_subq)) - .group_by(db.Paper.for_user, db.Paper.for_task, db.Paper.type) - .all() - ): - paper_counts[(user_id, task_id, type.name)] = count - task_sols: Dict[int, Dict[int, db.Solution]] = {} for t in tasks: task_sols[t.task_id] = {} @@ -1244,38 +1256,33 @@ def org_contest_solutions(ct_id: int, site_id: Optional[int] = None): task_sols[s.task_id][s.user_id] = s edit_form: Optional[ContestSolutionsEditForm] = None + edit_errors: Dict[str, str] = {} if edit_action: edit_form = ContestSolutionsEditForm() if edit_form.validate_on_submit(): - new_sol_count = 0 + edits = [] for task in tasks: for pion in pions: - if pion.user_id in task_sols[task.task_id]: - continue # již existuje - if not request.form.get(f"create_sol_{task.task_id}_{pion.user_id}"): - continue # nikdo nežádá o vytvoření + edits.append(SolEdit( + key=f'{task.task_id}_{pion.user_id}', + pion=pion, + task=task, + sol=task_sols[task.task_id].get(pion.user_id, None), + )) + if process_solution_edits(ctx, edits, edit_errors): + return redirect(ctx.url_for('org_contest_solutions')) + if edit_errors: + flash("Některé změny se nepodařilo provést. Důvod uvidíte po najetí myší nad políčka s červeným okrajem.", "danger") - sol = db.Solution(task=task, user=pion.user) - sess.add(sol) - mo.util.log( - type=db.LogType.participant, - what=pion.user_id, - details={ - 'action': 'solution-created', - 'task': task.task_id, - }, - ) - app.logger.info(f"Řešení úlohy {task.code} od účastníka {pion.user_id} založeno") - new_sol_count += 1 - - if new_sol_count > 0: - sess.commit() - flash(inflect_by_number(new_sol_count, "Založeno", "Založena", "Založeno") + ' ' - + inflect_number(new_sol_count, "nové řešení", "nová řešení", "nových řešení"), - "success") - else: - flash("Žádné změny k uložení", "info") - return redirect(ctx.url_for('org_contest_solutions')) + # Count papers for each task and solution + paper_counts = {} + for user_id, task_id, type, count in ( + db.get_session().query(db.Paper.for_user, db.Paper.for_task, db.Paper.type, func.count(db.Paper.type)) + .filter(db.Paper.for_task.in_(tasks_subq)) + .group_by(db.Paper.for_user, db.Paper.for_task, db.Paper.type) + .all() + ): + paper_counts[(user_id, task_id, type.name)] = count return render_template( 'org_contest_solutions.html', @@ -1283,7 +1290,8 @@ def org_contest_solutions(ct_id: int, site_id: Optional[int] = None): contest=ctx.contest, site=ctx.site, rights=ctx.rights, pions=pions, tasks=tasks, tasks_sols=task_sols, paper_counts=paper_counts, paper_link=lambda u, p: mo.web.util.org_paper_link(ctx.contest, ctx.site, u, p), - edit_form=edit_form, + edit_form=edit_form, request_form=request.form, + edit_errors=edit_errors, ) diff --git a/mo/web/templates/org_contest_solutions.html b/mo/web/templates/org_contest_solutions.html index ccef75c4..5e595c3a 100644 --- a/mo/web/templates/org_contest_solutions.html +++ b/mo/web/templates/org_contest_solutions.html @@ -5,12 +5,15 @@ {% set edit_points = not ctx.site and rights.can_edit_points() %} {% set edit_create = rights.can_create_solutions() %} {% set edit_both = edit_points and edit_create %} +{% set edit = edit_points or edit_create %} +{% set edit_title = "Editace řešení" if edit_form else "Odevzdaná řešení" %} +{% set edit_tip = 'Založit řešení nebo editovat body' if edit_both else 'Editovat body' if edit_points else 'Založit řešení' %} {% block title %} -{{ "Založení řešení" if edit_form else "Tabulka řešení" }} {% if site %}soutěžního místa {{ site.name }}{% else %}{{ contest.place.name_locative() }}{% endif %} +{{ edit_title }} {% if site %}soutěžního místa {{ site.name }}{% else %}{{ contest.place.name_locative() }}{% endif %} {% endblock %} {% block breadcrumbs %} -{{ ctx.breadcrumbs(action="Založení řešení" if edit_form else "Tabulka řešení") }} +{{ ctx.breadcrumbs(action=edit_title) }} {% endblock %} {% block pretitle %} @@ -26,9 +29,19 @@ <p><i> {% if edit_form %} -Zaškrtnutím políček u řešení, která dosud neexistují, a odesláním tlačítkem pod tabulkou tato řešení založíte. -To se hodí, pokud se nechystáte do systému nahrávat soubory řešení, ale jen chcete řešení vytvořit, aby jim -bylo možné vyplnit body. Pokud nějaké řešení založíte omylem, lze toto prázdné řešení smazat v jeho detailu. +{% if edit_points %} +Zde můžete bodovat odevzdaná řešení. +{% else %} +Zde můžete zakládat a rušit odevzdaná řešení. +{% endif %} +Prázdná hodnota ve sloupečku „Přidělené body“ znamená „neobodováno“, +hodnota X znamená „řešení nebylo odevzdáno“, +P znamená „prázdný protokol, který neobsahuje řešení“. +{% if edit_create %} +Smazáním X založíte nové řešení – to se hodí, pokud se nechystáte do systému nahrávat soubory řešení, +ale jen chcete řešení vytvořit, aby jim bylo možné vyplnit body. +Pokud nějaké řešení založíte omylem, lze toto prázdné řešení smazat přepsáním bodů zpět na X. +{% endif %} {% else %} Všechna odevzdání od účastníka k úloze můžete vidět po kliknutí na ikonku <span class="icon">🔍</span>. Tamtéž můžete nahrávat skeny jednotlivých řešení. @@ -54,12 +67,15 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje {% for task in tasks %}<th colspan=4> <a href="{{ ctx.url_for('org_contest_task', task_id=task.task_id) }}">{{ task.code }}</a> {% endfor %} + {% if not edit_form %} <th rowspan=2>Body celkem + {% endif %} </tr> <tr> {% for task in tasks %}<th title="Řešení">Řeš<th title="Oprava">Opr<th title="Body">B<th title="Detail"><span class="icon">🔍</span>{% endfor %} </tr> </thead> + {% set tabindex = namespace(value=1) %} {% for pion in pions %} {% set u = pion.user %} <tr class="state-{{ pion.state.name }}"> @@ -67,8 +83,8 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje <td>{{ pion.state.friendly_name() }} {% set sum_points = [] %} {% for task in tasks %} - {% if u.user_id in tasks_sols[task.task_id] %} - {% set sol = tasks_sols[task.task_id][u.user_id] %} + {% set sol = tasks_sols[task.task_id].get(u.user_id) %} + {% if sol %} {% if sol.final_submit_obj %} {% set p = sol.final_submit_obj %} {% set late = p.check_deadline(round) %} @@ -92,25 +108,43 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje {% else %} <td class="sol"> {% endif %} + {% elif edit_form %} + <td>– + <td>– + {% else %} + <td colspan=3>– + {% endif %} + {% if edit_form %} + {% set key = '{}_{}'.format(task.task_id, pion.user_id) %} + {% set def_pts = sol | sol_editable_points %} + {% set new_pts = request_form.get("points_{}".format(key)) %} + {% set err = edit_errors[key] %} + <td><input + type="text" class="form-control{% if err %} has-error{% endif %}" name="points_{{key}}" + value="{{ new_pts|none_value(def_pts) }}" + oninput="this.classList.toggle('modified', this.value != '{{ def_pts }}')" + size="4" tabindex={{ tabindex.value }} autofocus + data-sol="{% if sol %}1{% else %}0{% endif %}" + {% if not edit_create %} + disabled + {% endif %} + {% if err %} + title="{{ err }}" + {% endif %} + > + {% elif sol %} <td class="sol"> {{ sol|sol_display_points }} {% if sol.points is not none %}{% do sum_points.append(sol.points) %}{% endif %} - <td class="sol"> - {% else %} - <td colspan=3> - {% if edit_form %} - <label> - <input type="checkbox" name="create_sol_{{task.task_id}}_{{u.user_id}}"> - Založit - </label> - {% else %}–{% endif %} - <td> {% endif %} - <a class="btn btn-xs btn-link icon" title="Detail řešení" href="{{ ctx.url_for('org_submit_list', user_id=u.user_id, task_id=task.task_id) }}">🔍</a> + <td{% if sol %} class="sol"{% endif %}><a class="btn btn-xs btn-link icon" title="Detail řešení" href="{{ ctx.url_for('org_submit_list', user_id=u.user_id, task_id=task.task_id) }}">🔍</a> {% endfor %} + {% if not edit_form %} <th>{{ sum_points|sum|decimal }}</th> + {% endif %} </tr> {% endfor %} + {% if not edit_form %} <tfoot> <tr><td><td> {% for task in tasks %} @@ -120,15 +154,17 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje <a class='btn btn-xs btn-primary' href="{{ ctx.url_for('org_generic_batch_upload', task_id=task.task_id) }}">Nahrát</a> {% endif %} {% if edit_points or edit_create %} - <a class='btn btn-xs btn-primary' title="{{'Založit řešení nebo editovat body' if edit_both else 'Editovat body' if edit_points else 'Založit řešení'}}" href="{{ ctx.url_for('org_contest_task_edit', task_id=task.task_id) }}">Editovat</a> + <a class='btn btn-xs btn-primary' title="{{ edit_tip }}" href="{{ ctx.url_for('org_contest_task_edit', task_id=task.task_id) }}">Editovat</a> {% endif %} </div> {% endfor %} <td> </tfoot> + {% endif %} </table> {% if edit_form %} + {% include "parts/org_solution_js.html" %} <div class='btn-group'> {{ wtf.form_field(edit_form.submit, class="btn btn-primary") }} <a class="btn btn-default" href="{{ ctx.url_for('org_contest_solutions') }}">Zrušit</a> @@ -137,7 +173,7 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje {% else %} <div class='btn-group'> {% if rights.can_create_solutions() %} - <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_solutions_edit') }}">Založit řešení hromadně</a> + <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_solutions_edit') }}" title="{{ edit_tip }}">Editovat</a> {% endif %} </div> {% endif %} diff --git a/mo/web/templates/parts/org_solution_table.html b/mo/web/templates/parts/org_solution_table.html index 3b3252ad..7bcd89b7 100644 --- a/mo/web/templates/parts/org_solution_table.html +++ b/mo/web/templates/parts/org_solution_table.html @@ -121,9 +121,10 @@ finální (ve výchozím stavu poslední nahrané).{% elif rights.can_upload_sol {% endif %} <td class="text-center"> {% if edit %} + {% set key = u.user_id | string %} {% set def_pts = sol | sol_editable_points %} - {% set new_pts = request_form.get("points_{}".format(u.user_id)) %} - {% set err = edit_errors[u.user_id] %} + {% set new_pts = request_form.get("points_{}".format(key)) %} + {% set err = edit_errors[key] %} <input type="text" class="form-control{% if err %} has-error{% endif %}" name="points_{{u.user_id}}" value="{{ new_pts|none_value(def_pts) }}" -- GitLab