diff --git a/cms/__init__.py b/cms/__init__.py index 8f17c113a9..375e83cceb 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -5,6 +5,7 @@ # Copyright © 2010-2012 Stefano Maggiolo # Copyright © 2010-2012 Matteo Boscariol # Copyright © 2013-2014 Luca Wehrstedt +# Copyright © 2022 Vytis Banaitis # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -33,6 +34,7 @@ "TOKEN_MODE_DISABLED", "TOKEN_MODE_FINITE", "TOKEN_MODE_INFINITE", "TOKEN_MODE_MIXED", "FEEDBACK_LEVEL_FULL", "FEEDBACK_LEVEL_RESTRICTED", + "PARTICIPATION_LOCATION_ONSITE", "PARTICIPATION_LOCATION_REMOTE", # log # Nothing intended for external use, no need to advertise anything. # conf @@ -69,6 +71,11 @@ # can be omitted). FEEDBACK_LEVEL_RESTRICTED = "restricted" +# Participation location + +PARTICIPATION_LOCATION_ONSITE = "onsite" +PARTICIPATION_LOCATION_REMOTE = "remote" + from .conf import Address, ServiceCoord, ConfigError, async_config, config from .util import mkdir, rmtree, utf8_decoder, get_safe_shard, \ diff --git a/cms/conf.py b/cms/conf.py index e928243051..b0391dff7c 100644 --- a/cms/conf.py +++ b/cms/conf.py @@ -159,6 +159,7 @@ def __init__(self): self.teacher_login_kind = "district" # "district" or "school" self.teacher_allow_impersonate = False self.teacher_show_results = True + self.teacher_enable_participation_locations = False self.teacher_show_task_statements = "never" # "never", "after_start" or "always" self.teacher_allow_registration = False self.teacher_registration_anonymous = False diff --git a/cms/db/user.py b/cms/db/user.py index c98e97708f..ace5689c43 100644 --- a/cms/db/user.py +++ b/cms/db/user.py @@ -5,7 +5,7 @@ # Copyright © 2010-2018 Stefano Maggiolo # Copyright © 2010-2012 Matteo Boscariol # Copyright © 2012-2018 Luca Wehrstedt -# Copyright © 2014-2016 Vytis Banaitis +# Copyright © 2014-2022 Vytis Banaitis # Copyright © 2015 William Di Luigi # Copyright © 2016 Myungwoo Chun # @@ -33,8 +33,9 @@ from sqlalchemy.schema import Column, ForeignKey, CheckConstraint, \ UniqueConstraint from sqlalchemy.types import Boolean, Integer, String, Unicode, DateTime, \ - Interval + Interval, Enum +from cms import PARTICIPATION_LOCATION_ONSITE, PARTICIPATION_LOCATION_REMOTE from cmscommon.crypto import generate_random_password, build_password from . import CastingArray, Codename, Base, Admin, Contest, District, School @@ -234,6 +235,12 @@ class Participation(Base): nullable=False, default=False) + # Where the user is participating from and how supervised they are. + location = Column( + Enum(PARTICIPATION_LOCATION_ONSITE, PARTICIPATION_LOCATION_REMOTE, + name="participation_location"), + nullable=True) + # An unrestricted participation (e.g. contest time, # maximum number of submissions, minimum interval between submissions, # maximum number of user tests, minimum interval between user tests), diff --git a/cms/locale/cms.pot b/cms/locale/cms.pot index 9c27783960..606afd55e4 100644 --- a/cms/locale/cms.pot +++ b/cms/locale/cms.pot @@ -1145,3 +1145,12 @@ msgid "" "email address. After receiving confirmation we will send details to your " "email address." msgstr "" + +msgid "Participation location" +msgstr "" + +msgid "On-site" +msgstr "" + +msgid "Remote" +msgstr "" diff --git a/cms/locale/lt/LC_MESSAGES/cms.po b/cms/locale/lt/LC_MESSAGES/cms.po index 936a1b3931..becdb7cb87 100644 --- a/cms/locale/lt/LC_MESSAGES/cms.po +++ b/cms/locale/lt/LC_MESSAGES/cms.po @@ -1222,3 +1222,12 @@ msgstr "" "el. paštą. Tokiu atveju į mokyklos oficialų el. paštą išsiųsime prašymą " "patvirtinti pageidaujamą el. paštą. Tik sulaukę patvirtinimo, galėsime " "siųsti informaciją į pageidaujamą el. paštą." + +msgid "Participation location" +msgstr "Sprendimo vieta" + +msgid "On-site" +msgstr "Mokykla" + +msgid "Remote" +msgstr "Namai" diff --git a/cms/server/admin/handlers/contestuser.py b/cms/server/admin/handlers/contestuser.py index 1706ce8f50..8f91dbe125 100644 --- a/cms/server/admin/handlers/contestuser.py +++ b/cms/server/admin/handlers/contestuser.py @@ -11,6 +11,7 @@ # Copyright © 2016 Myungwoo Chun # Copyright © 2016 Peyman Jabbarzade Ganje # Copyright © 2017 Valentin Rosca +# Copyright © 2022 Vytis Banaitis # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -218,6 +219,7 @@ def post(self, contest_id, user_id): self.get_timedelta_sec(attrs, "extra_time") self.get_bool(attrs, "hidden") self.get_bool(attrs, "unrestricted") + self.get_string(attrs, "location", empty=None) # Update the participation. participation.set_attrs(attrs) diff --git a/cms/server/admin/templates/participation.html b/cms/server/admin/templates/participation.html index f4e5e22eee..62fc2c3a6f 100644 --- a/cms/server/admin/templates/participation.html +++ b/cms/server/admin/templates/participation.html @@ -135,6 +135,19 @@

Participation information< + + + + Participation location + + + + + diff --git a/cms/server/jinja2_toolbox.py b/cms/server/jinja2_toolbox.py index 707daec0ad..28dee9e4c7 100644 --- a/cms/server/jinja2_toolbox.py +++ b/cms/server/jinja2_toolbox.py @@ -3,6 +3,7 @@ # Contest Management System - http://cms-dev.github.io/ # Copyright © 2018 Luca Wehrstedt # Copyright © 2018 Stefano Maggiolo +# Copyright © 2022 Vytis Banaitis # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -28,7 +29,8 @@ contextfunction, environmentfunction from cms import TOKEN_MODE_DISABLED, TOKEN_MODE_FINITE, TOKEN_MODE_INFINITE, \ - TOKEN_MODE_MIXED, FEEDBACK_LEVEL_FULL, FEEDBACK_LEVEL_RESTRICTED + TOKEN_MODE_MIXED, FEEDBACK_LEVEL_FULL, FEEDBACK_LEVEL_RESTRICTED, \ + PARTICIPATION_LOCATION_ONSITE, PARTICIPATION_LOCATION_REMOTE from cms.db import SubmissionResult, UserTestResult from cms.grading import format_status_text from cms.grading.languagemanager import get_language @@ -152,6 +154,9 @@ def instrument_generic_toolbox(env): env.globals["FEEDBACK_LEVEL_FULL"] = FEEDBACK_LEVEL_FULL env.globals["FEEDBACK_LEVEL_RESTRICTED"] = FEEDBACK_LEVEL_RESTRICTED + env.globals["PARTICIPATION_LOCATION_ONSITE"] = PARTICIPATION_LOCATION_ONSITE + env.globals["PARTICIPATION_LOCATION_REMOTE"] = PARTICIPATION_LOCATION_REMOTE + env.filters["all"] = all_ env.filters["any"] = any_ env.filters["dictselect"] = dictselect diff --git a/cms/server/teacher/handlers/__init__.py b/cms/server/teacher/handlers/__init__.py index 71a82ea444..52ece3560f 100644 --- a/cms/server/teacher/handlers/__init__.py +++ b/cms/server/teacher/handlers/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Contest Management System - http://cms-dev.github.io/ -# Copyright © 2014-2020 Vytis Banaitis +# Copyright © 2014-2022 Vytis Banaitis # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -26,6 +26,7 @@ TaskStatementHandler, \ TaskAttachmentHandler, \ ContestAttachmentHandler, \ + ContestantLocationHandler, \ ImpersonateHandler @@ -40,6 +41,7 @@ (r"/contest/([0-9]+)/task/(.+)/attachment/(.+)", TaskAttachmentHandler), (r"/contest/([0-9]+)/attachment/(.+)", ContestAttachmentHandler), (r"/impersonate/([0-9]+)", ImpersonateHandler), + (r"/participation_location/([0-9]+)", ContestantLocationHandler), ] diff --git a/cms/server/teacher/handlers/contest.py b/cms/server/teacher/handlers/contest.py index 955fe3c65b..f3c12a4633 100644 --- a/cms/server/teacher/handlers/contest.py +++ b/cms/server/teacher/handlers/contest.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Contest Management System - http://cms-dev.github.io/ -# Copyright © 2014-2020 Vytis Banaitis +# Copyright © 2014-2022 Vytis Banaitis # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -32,7 +32,7 @@ import tornado.web as tornado_web from sqlalchemy.orm import contains_eager, joinedload, subqueryload -from cms import config +from cms import config, PARTICIPATION_LOCATION_ONSITE, PARTICIPATION_LOCATION_REMOTE from cms.db import Contest, Participation, Task, User from cms.grading.scoring import task_score from cms.server import FileHandlerMixin @@ -161,6 +161,13 @@ def get(self, contest_id, format="online"): self.r_params["header"] = header self.r_params["table"] = table self.r_params["allow_impersonate"] = config.teacher_allow_impersonate + self.r_params["enable_participation_location"] = ( + config.teacher_enable_participation_locations + ) + self.r_params["enable_participation_location_edit"] = ( + config.teacher_enable_participation_locations and + contest.phase(self.timestamp) <= 0 + ) self.render("contest.html", **self.r_params) @@ -243,6 +250,49 @@ def get(self, contest_id, filename): self.fetch(attachment, mimetype, filename) +class ContestantLocationHandler(BaseHandler): + """Set contestant participation location. + + """ + @tornado_web.authenticated + def post(self, participation_id): + if not config.teacher_enable_participation_locations: + raise tornado_web.HTTPError(403) + + p = Participation.get_from_id(participation_id, self.sql_session) + if p is None: + raise tornado_web.HTTPError(404) + if (p.contest_id not in config.teacher_active_contests or + userattr(p.user) != self.current_user): + raise tornado_web.HTTPError(403) + + return_url = self.url("contest", p.contest.id) + + if p.contest.phase(self.timestamp) > 0: + return self.redirect(return_url) + + location = self.get_argument("location", "") + if location not in (PARTICIPATION_LOCATION_ONSITE, PARTICIPATION_LOCATION_REMOTE): + raise tornado_web.HTTPError(400) + + try: + ip_address = ipaddress.ip_address(self.request.remote_ip) + except ValueError: + logger.warning("Invalid IP address provided by Tornado: %s", + self.request.remote_ip) + return None + + p.location = location + logger.info("Teacher set location to %s for contestant %r on contest %s, " + "from IP address %s, at %s.", + location, p.user.username, p.contest.name, ip_address, + self.timestamp) + + self.sql_session.commit() + + return self.redirect(return_url) + + class ImpersonateHandler(BaseHandler): """Impersonate a contestant. diff --git a/cms/server/teacher/templates/contest.html b/cms/server/teacher/templates/contest.html index 6ab8894293..0ad6e1eb30 100644 --- a/cms/server/teacher/templates/contest.html +++ b/cms/server/teacher/templates/contest.html @@ -65,6 +65,9 @@

{% trans %}Attachments{% endtrans %}

{% for head in header %} {{ head }} {% endfor %} + {% if enable_participation_location %} + {% trans %}Participation location{% endtrans %} + {% endif %} {% if allow_impersonate %} {% endif %} @@ -76,6 +79,23 @@

{% trans %}Attachments{% endtrans %}

{% for item in row %} {{ item }} {% endfor %} + {% if enable_participation_location %} + + {% if enable_participation_location_edit %} +
+ {{ xsrf_form_html|safe }} + + +
+ {% else %} + {% if p.location == PARTICIPATION_LOCATION_ONSITE %} + {% trans %}On-site{% endtrans %} + {% elif p.location == PARTICIPATION_LOCATION_REMOTE %} + {% trans %}Remote{% endtrans %} + {% endif %} + {% endif %} + + {% endif %} {% if allow_impersonate %} {% trans %}Log in as contestant{% endtrans %} {% endif %} diff --git a/config/cms.conf.sample b/config/cms.conf.sample index 9dd69f0029..f810cf0ac6 100644 --- a/config/cms.conf.sample +++ b/config/cms.conf.sample @@ -181,6 +181,10 @@ "_help": "Whether to show contestants' results to teachers.", "teacher_show_results": true, + "_help": "Whether to allow teachers to set contestants' participation", + "_help": "location (on-site vs remote).", + "teacher_enable_participation_locations": true, + "_help": "Whether and when to allow teachers to download task statements", "_help": "and attachments. May be 'never', 'after_start', or 'always'.", "teacher_show_task_statements": "never",