Skip to content
Snippets Groups Projects
Unverified Commit 94a6c4ba authored by Grace Hawkins's avatar Grace Hawkins Committed by GitHub
Browse files

Join contest with current account (#1191)


* Update user interface to support joining to contests

There are 2 tabs, one for creating new user and one for register
with an existing account. Note that the register page sould be
scrollable because of the long form inside it. So it can not use
login_box class.

* Implement logic of join contest without user creation

* Add contributor's name and copyright

* Check participations only when joining a contest

* Disable and hide register fields when joining to contest

The previous method had 2 different forms but the new method has a single
form and just disables extra fields if they're unused

* Clear field errors after each tab switch

* Small updates

Co-authored-by: default avatarAndrey Vihrov <andrey.vihrov@gmail.com>
parent bc2a337b
No related branches found
No related tags found
No related merge requests found
......@@ -46,6 +46,7 @@ Valentin Rosca <rosca.valentin2012@gmail.com>
Alexander Kernozhitsky <sh200105@mail.ru>
Benjamin Swart <Benjaminswart@email.cz>
Andrey Vihrov <andrey.vihrov@gmail.com>
Grace Hawkins <amoomajid99@gmail.com>
And many other people that didn't write code, but provided useful
comments, suggestions and feedback. :-)
......@@ -9,6 +9,7 @@
# Copyright © 2014 Artem Iglikov <artem.iglikov@gmail.com>
# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com>
# Copyright © 2015-2018 William Di Luigi <williamdiluigi@gmail.com>
# Copyright © 2021 Grace Hawkins <amoomajid99@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
......@@ -46,7 +47,7 @@ from cms.server.contest.authentication import validate_login
from cms.server.contest.communication import get_communications
from cms.server.contest.printing import accept_print_job, PrintingDisabled, \
UnacceptablePrintJob
from cmscommon.crypto import hash_password
from cmscommon.crypto import hash_password, validate_password
from cmscommon.datetime import make_datetime, make_timestamp
from .contest import ContestHandler
from ..phase_management import actual_phase_required
......@@ -72,7 +73,8 @@ class MainHandler(ContestHandler):
class RegistrationHandler(ContestHandler):
"""Registration handler.
Used to create a user account (and participation) when this is allowed.
Used to create a participation when this is allowed.
If `new_user` argument is true, it creates a new user too.
"""
......@@ -84,6 +86,46 @@ class RegistrationHandler(ContestHandler):
if not self.contest.allow_registration:
raise tornado_web.HTTPError(404)
create_new_user = self.get_argument("new_user") == "true"
# Get or create user
if create_new_user:
user = self._create_user()
else:
user = self._get_user()
# Check if the participation exists
contest = self.contest
tot_participants = self.sql_session.query(Participation)\
.filter(Participation.user == user)\
.filter(Participation.contest == contest)\
.count()
if tot_participants > 0:
raise tornado_web.HTTPError(409)
# Create participation
team = self._get_team()
participation = Participation(user=user, contest=self.contest,
team=team)
self.sql_session.add(participation)
self.sql_session.commit()
self.finish(user.username)
@multi_contest
def get(self):
if not self.contest.allow_registration:
raise tornado_web.HTTPError(404)
self.r_params["MAX_INPUT_LENGTH"] = self.MAX_INPUT_LENGTH
self.r_params["MIN_PASSWORD_LENGTH"] = self.MIN_PASSWORD_LENGTH
self.r_params["teams"] = self.sql_session.query(Team)\
.order_by(Team.name).all()
self.render("register.html", **self.r_params)
def _create_user(self):
try:
first_name = self.get_argument("first_name")
last_name = self.get_argument("last_name")
......@@ -110,18 +152,6 @@ class RegistrationHandler(ContestHandler):
# Override password with its hash
password = hash_password(password)
# If we have teams, we assume that the 'team' field is mandatory
if self.sql_session.query(Team).count() > 0:
try:
team_code = self.get_argument("team")
team = self.sql_session.query(Team)\
.filter(Team.code == team_code)\
.one()
except (tornado_web.MissingArgumentError, NoResultFound):
raise tornado_web.HTTPError(400)
else:
team = None
# Check if the username is available
tot_users = self.sql_session.query(User)\
.filter(User.username == username).count()
......@@ -129,29 +159,43 @@ class RegistrationHandler(ContestHandler):
# HTTP 409: Conflict
raise tornado_web.HTTPError(409)
# Store new user and participation
# Store new user
user = User(first_name, last_name, username, password, email=email)
self.sql_session.add(user)
participation = Participation(user=user, contest=self.contest,
team=team)
self.sql_session.add(participation)
self.sql_session.commit()
return user
self.finish(username)
def _get_user(self):
username = self.get_argument("username")
password = self.get_argument("password")
@multi_contest
def get(self):
if not self.contest.allow_registration:
# Find user if it exists
user = self.sql_session.query(User)\
.filter(User.username == username)\
.first()
if user is None:
raise tornado_web.HTTPError(404)
self.r_params["MAX_INPUT_LENGTH"] = self.MAX_INPUT_LENGTH
self.r_params["MIN_PASSWORD_LENGTH"] = self.MIN_PASSWORD_LENGTH
self.r_params["teams"] = self.sql_session.query(Team)\
.order_by(Team.name).all()
# Check if password is correct
if not validate_password(user.password, password):
raise tornado_web.HTTPError(403)
self.render("register.html", **self.r_params)
return user
def _get_team(self):
# If we have teams, we assume that the 'team' field is mandatory
if self.sql_session.query(Team).count() > 0:
try:
team_code = self.get_argument("team")
team = self.sql_session.query(Team)\
.filter(Team.code == team_code)\
.one()
except (tornado_web.MissingArgumentError, NoResultFound):
raise tornado_web.HTTPError(400)
else:
team = None
return team
class LoginHandler(ContestHandler):
......
......@@ -219,6 +219,21 @@ div.login_box {
}
}
/** Register **/
div.register_box form {
margin-bottom: 0;
}
div.register_box form fieldset div.control-group:last-child {
margin-bottom: 0;
}
div.register_box {
max-width: 450px;
margin: 20px auto;
}
/* Some fixes and enhancements of Bootstrap */
@media (max-width: 767px) {
......
......@@ -7,16 +7,58 @@
{% block body %}
<script>
$(document).ready(function() {
$("#signup").submit(function(e) {
const registerFields = [
"first_name",
"last_name",
"email",
"password-confirm",
];
function clearErrors() {
// Reset error text
$("#password-input").removeClass("error");
$("#password-input span.help-block").text("");
$("#password-confirm-input").removeClass("error");
$("#password-confirm-input span.help-block").text("");
$("#username-input").removeClass("error");
$("#username-input span.help-block").text("");
}
$(document).ready(function() {
const passwordHelp = $("#password-input span.help-block").text();
$("#register-pill").click(function(e) {
clearErrors();
$("#password-input span.help-block").text(passwordHelp);
$('input[name=new_user]').val(true);
registerFields.forEach((fieldName) => {
$(`#${fieldName}`).prop("disabled", false);
$(`#${fieldName}`).closest(".control-group").show();
});
$("#form-divider").after($("#credentials"));
$("#form-divider").before($("#profile"));
$("#form-divider").show();
});
$("#join-pill").click(function(e) {
clearErrors();
$('input[name=new_user]').val(false);
registerFields.forEach((fieldName) => {
$(`#${fieldName}`).prop("disabled", true);
$(`#${fieldName}`).closest(".control-group").hide();
});
$("#form-divider").after($("#profile"));
$("#form-divider").before($("#credentials"));
$("#form-divider").hide();
});
$("#signup").submit(function(e) {
clearErrors();
let isNewUser = $('input[name=new_user]').val() == 'true';
// Check that passwords are the same
if ($("#password").val() !== $("#password-confirm").val()) {
if (isNewUser && $("#password").val() !== $("#password-confirm").val()) {
$("#password-confirm-input").addClass("error");
$("#password-confirm-input span.help-block").text("{% trans %}The passwords do not match!{% endtrans %}");
$("#password-confirm").focus();
......@@ -31,11 +73,28 @@ $(document).ready(function() {
$("#confirmed-mask").show();
$("#show-username").text(data);
}).fail(function(data) {
// Check if username is already used
if (data.status === 409) {
if (isNewUser) {
// Username is already used
$("#username-input").addClass("error");
$("#username-input span.help-block").text("{% trans %}This username is already taken, please choose a different one.{% endtrans %}");
$("#username").focus();
} else {
// User is already participating in this contest
$("#username-input").addClass("error");
$("#username-input span.help-block").text("{% trans %}This user is already registered in the contest.{% endtrans %}");
$("#username").focus();
}
} else if (data.status === 404) {
// Invalid username
$("#username-input").addClass("error");
$("#username-input span.help-block").text("{% trans %}No such user.{% endtrans %}");
$("#username").focus();
} else if (data.status === 403) {
// Password does not match
$("#password-input").addClass("error");
$("#password-input span.help-block").text("{% trans %}The password is not correct.{% endtrans %}");
$("#password").focus();
}
}).always(function() {
$("#submit-button-loading").hide();
......@@ -44,22 +103,27 @@ $(document).ready(function() {
e.preventDefault();
});
});
</script>
<div class="login_container">
<div class="login_box hero-unit" style="margin-top: -300px;">
<div class="register_box hero-unit">
<div id="signup-mask">
<h1>{% trans %}New user{% endtrans %}</h1>
<h1>{% trans %}Registration{% endtrans %}</h1>
<p>{% trans %}Please fill in the fields to register{% endtrans %}</p>
<ul class="nav nav-pills">
<li class="active" id="register-pill"><a href="#" data-toggle="tab">{% trans %}New user{% endtrans %}</a></li>
<li id="join-pill"><a href="#" data-toggle="tab">{% trans %}Join contest{% endtrans %}</a></li>
</ul>
<form id="signup" class="form-horizontal" action="{{ contest_url('register') }}">
{{ xsrf_form_html|safe }}
{% set next_page = handler.get_argument("next", none) %}
{% if next_page is not none %}
<input type="hidden" name="next" value="{{ next_page }}">
{% endif %}
<input type="hidden" name="new_user" value="true" />
<fieldset>
<div id="profile">
<div class="control-group">
<label class="control-label" for="first_name">{% trans %}First name{% endtrans %}</label>
<div class="controls">
......@@ -91,9 +155,11 @@ $(document).ready(function() {
</div>
</div>
{% endif %}
</div>
<hr>
<hr id="form-divider">
<div id="credentials">
<div id="username-input" class="control-group">
<label class="control-label" for="username">{% trans %}Username{% endtrans %}</label>
<div class="controls">
......@@ -101,7 +167,7 @@ $(document).ready(function() {
<span class="help-block"></span>
</div>
</div>
<div class="control-group">
<div id="password-input" class="control-group">
<label class="control-label" for="password">{% trans %}Password{% endtrans %}</label>
<div class="controls">
<input required minlength="{{ MIN_PASSWORD_LENGTH }}" maxlength="{{ MAX_INPUT_LENGTH }}" type="password" class="input-xlarge" name="password" id="password" autocomplete="new-password">
......@@ -121,6 +187,8 @@ $(document).ready(function() {
<span class="help-block"></span>
</div>
</div>
</div>
<br>
<div class="control-group">
<div class="controls">
<button type="submit" class="btn btn-primary btn-large">{% trans %}Register{% endtrans %}</button>
......@@ -132,21 +200,20 @@ $(document).ready(function() {
</div>
<div id="confirmed-mask" style="display: none;">
<h1>{% trans %}New user{% endtrans %}</h1>
<h1>{% trans %}Registration{% endtrans %}</h1>
<p>{% trans %}The user was created successfully!{% endtrans %}</p>
<p>{% trans %}Registered in the contest successfully!{% endtrans %}</p>
<p>{% trans %}Your username is:{% endtrans %}</p>
<p id="show-username" style="font-size: xx-large; font-family: monospace; font-weight: bold; text-align: center;">
</p>
<p>{% trans %}The password you chose was stored securely.{% endtrans %}</p>
<p>{% trans %}Your password is stored securely.{% endtrans %}</p>
<a href="{{ contest_url() }}" class="btn btn-success btn-large">{% trans %}Back to login{% endtrans %}</a>
</div>
</div>
</div>
{% endblock body %}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment