From 6c86c3d40f55752e6d4c528b79a8d7ad0e3dd2d6 Mon Sep 17 00:00:00 2001 From: Martin Mares <mj@ucw.cz> Date: Tue, 28 Sep 2021 20:49:43 +0200 Subject: [PATCH] =?UTF-8?q?Propojen=C3=AD=20s=20CMS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit U praktických úloh nabízíme místo formuláře na odevzdání přechod do CMS přes nově dodělaný single sign-on. --- etc/config.py.example | 5 ++ mo/web/jinja.py | 1 + mo/web/templates/user_contest_task.html | 33 +++++-- mo/web/user.py | 113 ++++++++++++++++-------- 4 files changed, 109 insertions(+), 43 deletions(-) diff --git a/etc/config.py.example b/etc/config.py.example index af31c885..5e3f8772 100644 --- a/etc/config.py.example +++ b/etc/config.py.example @@ -56,3 +56,8 @@ REG_TOKEN_VALIDITY = 10 # Aktuální ročník MO CURRENT_YEAR = 71 + +# Instance CMS, ve které žijí praktické programovací úlohy, a její SSO secret. +# Pokud se neuvede nebo je None, praktické úlohy nejde odevzdávat. +# CMS_ROOT = 'https://contest.kam.mff.cuni.cz/cms/' +# CMS_SSO_SECRET = 'BrumBrum' diff --git a/mo/web/jinja.py b/mo/web/jinja.py index 04cb8b0c..46cf724e 100644 --- a/mo/web/jinja.py +++ b/mo/web/jinja.py @@ -42,6 +42,7 @@ app.jinja_env.globals.update(LogType=db.LogType) app.jinja_env.globals.update(PartState=db.PartState) app.jinja_env.globals.update(RoleType=db.RoleType) app.jinja_env.globals.update(PaperType=db.PaperType) +app.jinja_env.globals.update(TaskType=db.TaskType) app.jinja_env.globals.update(JobType=db.JobType) app.jinja_env.globals.update(JobState=db.JobState) diff --git a/mo/web/templates/user_contest_task.html b/mo/web/templates/user_contest_task.html index 2521f054..2a6e4d01 100644 --- a/mo/web/templates/user_contest_task.html +++ b/mo/web/templates/user_contest_task.html @@ -23,15 +23,32 @@ {% if state == RoundState.running %} {% if contest.ct_can_submit() %} <h3>Odevzdat řešení</h3> - {% if round.ct_submit_end and g.now > round.ct_submit_end %} - <p class="alert alert-danger">Pozor, odevzdáváte po termínu, uplynul {{ round.ct_submit_end|time_and_timedelta }}. - Vaše řešení nemusí být hodnoceno. Doporučujeme využít políčko pro poznámku a vysvětlit situaci. + {% if task.type == TaskType.regular %} + {% if round.ct_submit_end and g.now > round.ct_submit_end %} + <p class="alert alert-danger">Pozor, odevzdáváte po termínu, uplynul {{ round.ct_submit_end|time_and_timedelta }}. + Vaše řešení nemusí být hodnoceno. Doporučujeme využít políčko pro poznámku a vysvětlit situaci. + {% else %} + <p>Pokud jste se spletli a nahráli nesprávné či neúplné řešení, můžete ho nahradit + řešením správným. V tom případě však uveďte do poznámky, proč jste řešení + nahradili (např. nahráli jste omylem řešení jiné úlohy). + {% endif %} + {{ wtf.quick_form(form, form_type='basic', button_map={'submit': 'primary'}) }} + {% elif task.type == TaskType.cms and cms_params %} + <p>Tato úloha je praktická a odevzdává se do systému CMS. Odevzdaná řešení + se zde objeví až s koncem soutěže. + + <form action='{{ cms_params.url }}' method=POST> + <input type=hidden name=username value="{{ cms_params.username }}"> + <input type=hidden name=first_name value="{{ cms_params.first_name }}"> + <input type=hidden name=last_name value="{{ cms_params.last_name }}"> + <input type=hidden name=timestamp value="{{ cms_params.timestamp }}"> + <input type=hidden name=signature value="{{ cms_params.signature }}"> + <input type=hidden name=back_url value="{{ cms_params.back_url }}"> + <input type=submit class='btn btn-primary' value="Přejít do CMS"> + </form> {% else %} - <p>Pokud jste se spletli a nahráli nesprávné či neúplné řešení, můžete ho nahradit - řešením správným. V tom případě však uveďte do poznámky, proč jste řešení - nahradili (např. nahráli jste omylem řešení jiné úlohy). + <p>Úloha s neznámým režimem odevzdávání. {% endif %} - {{ wtf.quick_form(form, form_type='basic', button_map={'submit': 'primary'}) }} {% else %} <p>Již není možné odevzdat řešení, termín na odevzdávání vypršel.</p> {% endif %} @@ -57,7 +74,7 @@ <p>Soutěž se nachází v neznámém stavu. To by se nemělo stát :) {% endif %} -{% if sol or state == RoundState.running %} +{% if sol or state == RoundState.running and task.type == TaskType.regular %} <h3>Historie vašich řešení</h3> {% if papers %} diff --git a/mo/web/user.py b/mo/web/user.py index 0ff79c8f..0ba45334 100644 --- a/mo/web/user.py +++ b/mo/web/user.py @@ -1,9 +1,12 @@ +from dataclasses import dataclass from flask import render_template, jsonify, g, redirect, url_for, flash, request from flask_wtf import FlaskForm import flask_wtf.file +import hashlib +import hmac from sqlalchemy import and_ from sqlalchemy.orm import joinedload -from typing import List, Tuple +from typing import List, Tuple, Optional import werkzeug.exceptions import wtforms from wtforms.validators import Required @@ -342,6 +345,34 @@ class SubmitForm(FlaskForm): submit = wtforms.SubmitField('Odevzdat') +@dataclass +class CMSParams: + url: str + username: str + first_name: str + last_name: str + timestamp: str + signature: str = "" + back_url: str = "" + + +def get_cms_params() -> Optional[CMSParams]: + if not (hasattr(config, 'CMS_ROOT') and hasattr(config, 'CMS_SSO_SECRET')): + return None + + p = CMSParams( + url=config.CMS_ROOT + 'sso-login', + username=f'osmo{g.user.user_id}', + first_name=g.user.first_name, + last_name=g.user.last_name, + timestamp=str(int(mo.now.timestamp())), + ) + msg = ":".join((p.username, p.first_name, p.last_name, p.timestamp)).encode('utf-8') + key = config.CMS_SSO_SECRET.encode('us-ascii') + p.signature = hmac.HMAC(key, msg, digestmod=hashlib.sha256).hexdigest() + return p + + @app.route('/user/contest/<int:contest_id>/task/<int:task_id>/', methods=('GET', 'POST')) def user_contest_task(contest_id: int, task_id: int): contest = get_contest(contest_id) @@ -356,40 +387,51 @@ def user_contest_task(contest_id: int, task_id: int): # stránku, abychom něco neprozradili jménem úlohy raise werkzeug.exceptions.Forbidden() - form = SubmitForm() - if contest.ct_can_submit() and form.validate_on_submit(): - file = form.file.data.stream - paper = db.Paper(task=task, for_user_obj=g.user, uploaded_by_obj=g.user, type=db.PaperType.solution, note=form.note.data) - submitter = mo.submit.Submitter() - - try: - submitter.submit_paper(paper, file.name) - except mo.submit.SubmitException as e: - flash(f'Chyba: {e}', 'danger') - return redirect(url_for('user_contest_task', contest_id=contest_id, task_id=task_id)) - - sess.add(paper) - - # FIXME: Bylo by hezké použít INSERT ... ON CONFLICT UPDATE - # (SQLAlchemy to umí, ale ne přes ORM, jen core rozhraním) - sol = (sess.query(db.Solution) - .filter_by(task=task, user=g.user) - .with_for_update() - .one_or_none()) - if sol is None: - sol = db.Solution(task=task, user=g.user) - sess.add(sol) - sol.final_submit_obj = paper - - sess.commit() - - if paper.is_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)) + form: Optional[SubmitForm] = None + if task.type == db.TaskType.regular: + form = SubmitForm() + if contest.ct_can_submit() and form.validate_on_submit(): + if task.type != db.TaskType.regular: + raise werkzeug.exceptions.Forbidden() + + file = form.file.data.stream + paper = db.Paper(task=task, for_user_obj=g.user, uploaded_by_obj=g.user, type=db.PaperType.solution, note=form.note.data) + submitter = mo.submit.Submitter() + + try: + submitter.submit_paper(paper, file.name) + except mo.submit.SubmitException as e: + flash(f'Chyba: {e}', 'danger') + return redirect(url_for('user_contest_task', contest_id=contest_id, task_id=task_id)) + + sess.add(paper) + + # FIXME: Bylo by hezké použít INSERT ... ON CONFLICT UPDATE + # (SQLAlchemy to umí, ale ne přes ORM, jen core rozhraním) + sol = (sess.query(db.Solution) + .filter_by(task=task, user=g.user) + .with_for_update() + .one_or_none()) + if sol is None: + sol = db.Solution(task=task, user=g.user) + sess.add(sol) + sol.final_submit_obj = paper + + sess.commit() + + if paper.is_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)) + + cms_params: Optional[CMSParams] = None + if task.type == db.TaskType.cms: + cms_params = get_cms_params() + if cms_params: + cms_params.back_url = url_for('user_contest_task', contest_id=contest_id, task_id=task_id) sol = sess.query(db.Solution).filter_by(task=task, user=g.user).one_or_none() @@ -406,6 +448,7 @@ def user_contest_task(contest_id: int, task_id: int): sol=sol, papers=papers, form=form, + cms_params=cms_params, messages=messages, ) -- GitLab