Skip to content

Commit

Permalink
TWS: allow teachers to set participation location for contestants
Browse files Browse the repository at this point in the history
  • Loading branch information
vytisb committed Feb 21, 2024
1 parent 2cd830f commit 681dbfd
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 6 deletions.
7 changes: 7 additions & 0 deletions cms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Copyright © 2010-2012 Stefano Maggiolo <[email protected]>
# Copyright © 2010-2012 Matteo Boscariol <[email protected]>
# Copyright © 2013-2014 Luca Wehrstedt <[email protected]>
# Copyright © 2022 Vytis Banaitis <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, \
Expand Down
1 change: 1 addition & 0 deletions cms/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions cms/db/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Copyright © 2010-2018 Stefano Maggiolo <[email protected]>
# Copyright © 2010-2012 Matteo Boscariol <[email protected]>
# Copyright © 2012-2018 Luca Wehrstedt <[email protected]>
# Copyright © 2014-2016 Vytis Banaitis <[email protected]>
# Copyright © 2014-2022 Vytis Banaitis <[email protected]>
# Copyright © 2015 William Di Luigi <[email protected]>
# Copyright © 2016 Myungwoo Chun <[email protected]>
#
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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),
Expand Down
9 changes: 9 additions & 0 deletions cms/locale/cms.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
9 changes: 9 additions & 0 deletions cms/locale/lt/LC_MESSAGES/cms.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions cms/server/admin/handlers/contestuser.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# Copyright © 2016 Myungwoo Chun <[email protected]>
# Copyright © 2016 Peyman Jabbarzade Ganje <[email protected]>
# Copyright © 2017 Valentin Rosca <[email protected]>
# Copyright © 2022 Vytis Banaitis <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions cms/server/admin/templates/participation.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ <h2 id="title_participation_info" class="toggling_on">Participation information<
</td>
<td><input type="text" name="extra_time" value="{{ participation.extra_time.total_seconds()|int }}"></td>
</tr>
<tr>
<td>
<span class="info" title="Where the user is participating from and how supervised they are."></span>
Participation location
</td>
<td>
<select name="location">
<option value="" {% if participation.location is none %}selected{% endif %}></option>
<option value="{{ PARTICIPATION_LOCATION_ONSITE }}" {% if participation.location == PARTICIPATION_LOCATION_ONSITE %}selected{% endif %}>On-site</option>
<option value="{{ PARTICIPATION_LOCATION_REMOTE }}" {% if participation.location == PARTICIPATION_LOCATION_REMOTE %}selected{% endif %}>Remote</option>
</select>
</td>
</tr>
</table>
<input type="submit" value="Update" />
<input type="reset" value="Reset" />
Expand Down
7 changes: 6 additions & 1 deletion cms/server/jinja2_toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Contest Management System - http://cms-dev.github.io/
# Copyright © 2018 Luca Wehrstedt <[email protected]>
# Copyright © 2018 Stefano Maggiolo <[email protected]>
# Copyright © 2022 Vytis Banaitis <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion cms/server/teacher/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3

# Contest Management System - http://cms-dev.github.io/
# Copyright © 2014-2020 Vytis Banaitis <[email protected]>
# Copyright © 2014-2022 Vytis Banaitis <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand All @@ -26,6 +26,7 @@
TaskStatementHandler, \
TaskAttachmentHandler, \
ContestAttachmentHandler, \
ContestantLocationHandler, \
ImpersonateHandler


Expand All @@ -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),
]


Expand Down
54 changes: 52 additions & 2 deletions cms/server/teacher/handlers/contest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3

# Contest Management System - http://cms-dev.github.io/
# Copyright © 2014-2020 Vytis Banaitis <[email protected]>
# Copyright © 2014-2022 Vytis Banaitis <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions cms/server/teacher/templates/contest.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ <h3>{% trans %}Attachments{% endtrans %}</h3>
{% for head in header %}
<th>{{ head }}</th>
{% endfor %}
{% if enable_participation_location %}
<th style="width: 1%">{% trans %}Participation location{% endtrans %}</th>
{% endif %}
{% if allow_impersonate %}
<th style="width: 1%"></th>
{% endif %}
Expand All @@ -76,6 +79,23 @@ <h3>{% trans %}Attachments{% endtrans %}</h3>
{% for item in row %}
<td>{{ item }}</td>
{% endfor %}
{% if enable_participation_location %}
<td style="white-space: nowrap">
{% if enable_participation_location_edit %}
<form class="form-horizontal" style="margin: 0" enctype="multipart/form-data" action="{{ url("participation_location", p.id) }}" method="POST">
{{ xsrf_form_html|safe }}
<button type="{% if p.location == PARTICIPATION_LOCATION_ONSITE %}button{% else %}submit{% endif %}" name="location" value="onsite" class="btn{% if p.location == PARTICIPATION_LOCATION_ONSITE %} active{% endif %}">{% trans %}On-site{% endtrans %}</button>
<button type="{% if p.location == PARTICIPATION_LOCATION_REMOTE %}button{% else %}submit{% endif %}" name="location" value="remote" class="btn{% if p.location == PARTICIPATION_LOCATION_REMOTE %} active{% endif %}">{% trans %}Remote{% endtrans %}</button>
</form>
{% else %}
{% if p.location == PARTICIPATION_LOCATION_ONSITE %}
{% trans %}On-site{% endtrans %}
{% elif p.location == PARTICIPATION_LOCATION_REMOTE %}
{% trans %}Remote{% endtrans %}
{% endif %}
{% endif %}
</td>
{% endif %}
{% if allow_impersonate %}
<td style="white-space: nowrap"><a href="{{ url("impersonate", p.id) }}" class="btn" target="_blank">{% trans %}Log in as contestant{% endtrans %}</a></td>
{% endif %}
Expand Down
4 changes: 4 additions & 0 deletions config/cms.conf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 681dbfd

Please sign in to comment.