diff --git a/cms/db/__init__.py b/cms/db/__init__.py index 31be9a62c5..685de593a8 100644 --- a/cms/db/__init__.py +++ b/cms/db/__init__.py @@ -110,7 +110,7 @@ from .util import test_db_connection, get_contest_list, is_contest_id, \ ask_for_contest, get_submissions, get_submission_results, \ - get_datasets_to_judge, enumerate_files + get_datasets_to_judge, enumerate_files, get_global_statement configure_mappers() diff --git a/cms/db/util.py b/cms/db/util.py index 28fd97ae69..53435ba0ba 100644 --- a/cms/db/util.py +++ b/cms/db/util.py @@ -337,3 +337,38 @@ def enumerate_files( digests = set(r[0] for r in session.execute(union(*queries))) digests.discard(Digest.TOMBSTONE) return digests + + +def get_global_statement(session, contest=None): + """Return the global statement of the contest, if it exists. + + If this contest has more than one task, each having a single statement, + and they're all the same, return that. Otherwise, return None.""" + if contest is None: + return None + + # just get everything! + # There's probably a more efficient way to do this though... + + # each task must have exactly one statement + if not all(len(task.statements) == 1 for task in contest.tasks): + return None + + statements = [(lang_code, statement) + for task in contest.tasks + for lang_code, statement in task.statements.items() + ] + + # there must be exactly one language code + if len({lang_code for lang_code, statement in statements}) != 1: + return None + + # there must be exactly one digest + if len({statement.digest for lang_code, statement in statements}) != 1: + return None + + # there must be more than one statement + if len(statements) < 2: + return None + + return next(statement for lang_code, statement in statements) diff --git a/cms/server/admin/handlers/__init__.py b/cms/server/admin/handlers/__init__.py index 093ea93b6a..a97df4f19e 100644 --- a/cms/server/admin/handlers/__init__.py +++ b/cms/server/admin/handlers/__init__.py @@ -44,6 +44,8 @@ QuestionClaimHandler from .contestranking import \ RankingHandler +from .conteststatement import \ + ContestStatementHandler from .contestsubmission import \ ContestSubmissionsHandler, \ ContestUserTestsHandler @@ -136,6 +138,10 @@ (r"/contest/([0-9]+)/tasks", ContestTasksHandler), (r"/contest/([0-9]+)/tasks/add", AddContestTaskHandler), + # Contest's global statement + + (r"/contest/([0-9]+)/globalstatement", ContestStatementHandler), + # Contest's submissions / user tests (r"/contest/([0-9]+)/submissions", ContestSubmissionsHandler), diff --git a/cms/server/admin/handlers/base.py b/cms/server/admin/handlers/base.py index e38be1fbbf..ed0ea5b7c4 100644 --- a/cms/server/admin/handlers/base.py +++ b/cms/server/admin/handlers/base.py @@ -43,7 +43,7 @@ from cms import __version__, config from cms.db import Admin, Contest, Participation, Question, Submission, \ - SubmissionResult, Task, Team, User, UserTest + SubmissionResult, Task, Team, User, UserTest, get_global_statement from cms.grading.scoretypes import get_score_type_class from cms.grading.tasktypes import get_task_type_class from cms.server import CommonRequestHandler, FileHandlerMixin @@ -316,6 +316,7 @@ def render_params(self): params["task_list"] = self.sql_session.query(Task).all() params["user_list"] = self.sql_session.query(User).all() params["team_list"] = self.sql_session.query(Team).all() + params["global_statement"] = get_global_statement(self.sql_session, self.contest) return params def write_error(self, status_code, **kwargs): diff --git a/cms/server/admin/handlers/conteststatement.py b/cms/server/admin/handlers/conteststatement.py new file mode 100644 index 0000000000..2c8257f168 --- /dev/null +++ b/cms/server/admin/handlers/conteststatement.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2010-2013 Giovanni Mascellani +# Copyright © 2010-2018 Stefano Maggiolo +# Copyright © 2010-2012 Matteo Boscariol +# Copyright © 2012-2018 Luca Wehrstedt +# Copyright © 2014 Artem Iglikov +# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> +# Copyright © 2016 Myungwoo Chun +# Copyright © 2023 Kevin Atienza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Statement-related handlers for AWS for a specific contest. + +""" + +try: + import tornado4.web as tornado_web +except ImportError: + import tornado.web as tornado_web + +from cms.db import Contest, Session, Statement, Task +from cmscommon.datetime import make_datetime +from .base import BaseHandler, require_permission + + +class ContestStatementHandler(BaseHandler): + """Add a global statement to a contest. + + """ + @require_permission(BaseHandler.PERMISSION_ALL) + def get(self, contest_id): + self.contest = self.safe_get_item(Contest, contest_id) + self.r_params = self.render_params() + self.render("add_global_statement.html", **self.r_params) + + @require_permission(BaseHandler.PERMISSION_ALL) + def post(self, contest_id): + fallback_page = self.url("contest", contest_id, "globalstatement") + + contest = self.safe_get_item(Contest, contest_id) + + language = self.get_argument("language", "") + if len(language) == 0: + self.service.add_notification( + make_datetime(), + "No language code specified", + "The language code can be any string.") + self.redirect(fallback_page) + return + statement = self.request.files["statement"][0] + if not statement["filename"].endswith(".pdf"): + self.service.add_notification( + make_datetime(), + "Invalid contest statement", + "The contest statement must be a .pdf file.") + self.redirect(fallback_page) + return + + contest_name = contest.name + self.sql_session.close() + + try: + digest = self.service.file_cacher.put_file_content( + statement["body"], + "Global statement for contest %s (lang: %s)" % ( + contest_name, + language)) + except Exception as error: + self.service.add_notification( + make_datetime(), + "Contest global statement storage failed", + repr(error)) + self.redirect(fallback_page) + return + + self.sql_session = Session() + + contest = self.safe_get_item(Contest, contest_id) + for task in contest.tasks: + self.sql_session\ + .query(Statement)\ + .filter(Statement.task_id == task.id)\ + .delete() + statement = Statement(language, digest, task=task) + self.sql_session.add(statement) + + if self.try_commit(): + self.redirect(self.url("contest", contest_id)) + else: + self.redirect(fallback_page) diff --git a/cms/server/admin/templates/add_global_statement.html b/cms/server/admin/templates/add_global_statement.html new file mode 100644 index 0000000000..f01b52f876 --- /dev/null +++ b/cms/server/admin/templates/add_global_statement.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block core %} +
+

{{ contest.name }} - Upload global statement

+

WARNING: THIS WILL OVERWRITE ALL EXISTING STATEMENTS!!

+
+
+ {{ xsrf_form_html|safe }} + Language code:
+
+ + +
+{% endblock core %} diff --git a/cms/server/admin/templates/contest.html b/cms/server/admin/templates/contest.html index b667213397..bb77b3d847 100644 --- a/cms/server/admin/templates/contest.html +++ b/cms/server/admin/templates/contest.html @@ -81,6 +81,22 @@

Contest configuration

+ + + + Global statement +{% if admin.permission_all %} + [set] +{% endif %} + + + {% if global_statement %} + Global statement + {% else %} + No global statement. + {% endif %} + +

Logging in

diff --git a/cms/server/contest/handlers/__init__.py b/cms/server/contest/handlers/__init__.py index 7f8d86b603..763a35fb01 100644 --- a/cms/server/contest/handlers/__init__.py +++ b/cms/server/contest/handlers/__init__.py @@ -26,6 +26,8 @@ from .communication import \ CommunicationHandler, \ QuestionHandler +from .contest import \ + ContestStatementViewHandler from .main import \ LoginHandler, \ LogoutHandler, \ @@ -66,6 +68,10 @@ (r"/printing", PrintingHandler), (r"/documentation", DocumentationHandler), + # Contest stuff + + (r"/globalstatement", ContestStatementViewHandler), + # Tasks (r"/tasks/(.*)/description", TaskDescriptionHandler), diff --git a/cms/server/contest/handlers/contest.py b/cms/server/contest/handlers/contest.py index 4fe6936ec4..7425a1f6c9 100644 --- a/cms/server/contest/handlers/contest.py +++ b/cms/server/contest/handlers/contest.py @@ -38,13 +38,13 @@ import tornado.web as tornado_web from cms import config, TOKEN_MODE_MIXED -from cms.db import Contest, Submission, Task, UserTest +from cms.db import Contest, Submission, Task, UserTest, get_global_statement from cms.locale import filter_language_codes -from cms.server import FileHandlerMixin +from cms.server import FileHandlerMixin, multi_contest from cms.server.contest.authentication import authenticate_request from cmscommon.datetime import get_timezone from .base import BaseHandler -from ..phase_management import compute_actual_phase +from ..phase_management import compute_actual_phase, actual_phase_required logger = logging.getLogger(__name__) @@ -161,6 +161,9 @@ def get_current_user(self): return participation + def get_global_statement(self): + return get_global_statement(self.sql_session, self.contest) + def render_params(self): ret = super().render_params() @@ -287,3 +290,23 @@ def notify_error(self, subject, text, text_params=None): class FileHandler(ContestHandler, FileHandlerMixin): pass + + +class ContestStatementViewHandler(FileHandler): + """Shows the global statement file of a contest. + + """ + @tornado_web.authenticated + @actual_phase_required(0, 3) + @multi_contest + def get(self): + statement = get_global_statement(self.sql_session, self.contest) + if statement is None: + raise tornado_web.HTTPError(404) + + digest = statement.digest + self.sql_session.close() + + filename = "%s.pdf" % self.contest.name + + self.fetch(digest, "application/pdf", filename) diff --git a/cms/server/contest/handlers/task.py b/cms/server/contest/handlers/task.py index 3f6f163a28..537ec8acd8 100644 --- a/cms/server/contest/handlers/task.py +++ b/cms/server/contest/handlers/task.py @@ -56,7 +56,12 @@ def get(self, task_name): if task is None: raise tornado_web.HTTPError(404) - self.render("task_description.html", task=task, **self.r_params) + global_statement = self.get_global_statement() + self.render( + "task_description.html", + task=task, + global_statement=global_statement, + **self.r_params) class TaskStatementViewHandler(FileHandler): diff --git a/cms/server/contest/templates/task_description.html b/cms/server/contest/templates/task_description.html index 618613a31b..2af18244a1 100644 --- a/cms/server/contest/templates/task_description.html +++ b/cms/server/contest/templates/task_description.html @@ -13,9 +13,17 @@

{% trans name=task.title, short_name=task.name %}{{ name }} ({{ short_name } -

{% trans %}Statement{% endtrans %}

+

{% if global_statement %}{% trans %}Statements{% endtrans %}{% else %}{% trans %}Statement{% endtrans %}{% endif %}

-{% if task.statements|length == 0 %} +{% if global_statement %} +
+
+ {% for lang_code in task.statements %} + {% trans %}Download contest statements (all tasks){% endtrans %} + {% endfor %} +
+
+{% elif task.statements|length == 0 %}
{% trans %}no statement available{% endtrans %}