Commit a7f87680 authored by Robert Czechowski's avatar Robert Czechowski
Browse files

Merge branch 'v1.8' into deploy

parents 0a98662c 367b5f56
Pipeline #1026 passed with stages
in 8 minutes and 23 seconds
......@@ -1064,7 +1064,7 @@ checksum = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48"
[[package]]
name = "medal"
version = "1.7.1"
version = "1.8.0"
dependencies = [
"bcrypt",
"csv",
......
[package]
version = "1.7.2"
version = "1.8.0"
name = "medal"
authors = ["Robert Czechowski <czechowski@bwinf.de>", "Daniel Brüning <bruening@bwinf.de>"]
......
......@@ -50,7 +50,7 @@ pub struct Config {
pub allow_sex_diverse: Option<bool>,
pub allow_sex_other: Option<bool>,
pub dbstatus_secret: Option<String>,
pub template_params: Option<::std::collections::BTreeMap<String, String>>,
pub template_params: Option<::std::collections::BTreeMap<String, serde_json::Value>>,
}
#[derive(StructOpt, Debug)]
......
......@@ -138,6 +138,7 @@ pub fn index<T: MedalConnection>(conn: &T, session_token: Option<String>, login_
fill_oauth_data(login_info, &mut data);
data.insert("parent".to_string(), to_json(&"base"));
data.insert("index".to_string(), to_json(&true));
Ok(("index".to_owned(), data))
}
......@@ -327,7 +328,7 @@ fn check_contest_constraints(session: &SessionUser, contest: &Contest) -> Contes
}
pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
query_string: Option<String>, login_info: LoginInfo)
query_string: Option<String>, login_info: LoginInfo, secret: Option<String>)
-> MedalValueResult
{
let session = conn.get_session_or_new(&session_token).unwrap();
......@@ -354,10 +355,26 @@ pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token
data.insert("message".to_string(), to_json(&contest.message));
fill_oauth_data(login_info, &mut data);
if secret.is_some() && secret != contest.secret {
return Err(MedalError::AccessDenied);
}
let mut require_secret = false;
if contest.secret.is_some() {
data.insert("secret_field".to_string(), to_json(&true));
if secret.is_some() {
data.insert("secret_field_prefill".to_string(), to_json(&secret));
} else {
require_secret = true;
}
}
let constraints = check_contest_constraints(&session, &contest);
let is_qualified = check_contest_qualification(conn, &session, &contest).unwrap_or(true);
let can_start = session.is_logged_in() && constraints.contest_running && constraints.grade_matching && is_qualified;
let can_start = constraints.contest_running && constraints.grade_matching && is_qualified
&& (session.is_logged_in() || contest.secret.is_some() && !contest.requires_login.unwrap_or(false));
let has_duration = contest.duration > 0;
data.insert("constraints".to_string(), to_json(&constraints));
......@@ -377,6 +394,7 @@ pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token
&& contest.duration == 0
&& constraints.contest_running
&& constraints.grade_matching
&& !require_secret
&& contest.requires_login != Some(true)
{
conn.new_participation(&session_token, contest_id).map_err(|_| MedalError::AccessDenied)?;
......@@ -517,14 +535,14 @@ pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, sessi
Ok(("contestresults".to_owned(), data))
}
pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str)
pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str, secret: Option<String>)
-> MedalResult<()> {
// TODO: Is _or_new the right semantic? We need a CSRF token anyway …
let session = conn.get_session_or_new(&session_token).unwrap();
let contest = conn.get_contest_by_id(contest_id);
// Check logged in or open contest
if contest.duration != 0 && !session.is_logged_in() {
if contest.duration != 0 && !session.is_logged_in() && (contest.requires_login.unwrap_or(false) || contest.secret.is_none()) {
return Err(MedalError::AccessDenied);
}
......@@ -546,6 +564,10 @@ pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_toke
return Err(MedalError::AccessDenied);
}
if contest.secret != secret {
return Err(MedalError::AccessDenied);
}
// Start contest
match conn.new_participation(&session_token, contest_id) {
Ok(_) => Ok(()),
......@@ -719,7 +741,7 @@ pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token
Ok("{}".to_string())
}
pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str) -> MedalValueResult {
pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str) -> Result<MedalValue, i32> {
let session = conn.get_session_or_new(&session_token).unwrap();
let (t, tg, c) = conn.get_task_by_id_complete(task_id);
......@@ -747,7 +769,7 @@ pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str
}
match conn.get_own_participation(&session_token, c.id.expect("Value from database")) {
None => Err(MedalError::AccessDenied),
None => Err(c.id.unwrap()),
Some(participation) => {
let now = time::get_time();
let passed_secs = now.sec - participation.start.sec;
......@@ -763,9 +785,8 @@ pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str
let left_secs = i64::from(c.duration) * 60 - passed_secs;
if c.duration > 0 && left_secs < 0 {
Err(MedalError::AccessDenied)
Err(c.id.unwrap())
// Contest over
// TODO: Nicer message!
} else {
let (hour, min, sec) = (left_secs / 3600, left_secs / 60 % 60, left_secs % 60);
......@@ -1016,6 +1037,8 @@ pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id:
}
if session.oauth_provider != Some("pms".to_string()) {
data.insert("profile_not_pms".into(), to_json(&true));
// This should be changed so that it can be configured if
// addresses can be obtained from OAuth provider
}
data.insert("ownprofile".into(), to_json(&true));
......
......@@ -72,7 +72,7 @@ fn read_contest(p: &Path) -> Option<Contest> {
let mut file = File::open(p).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
file.read_to_string(&mut contents).ok()?;
contestreader_yaml::parse_yaml(&contents,
p.file_name().to_owned()?.to_str()?,
......@@ -82,8 +82,13 @@ fn read_contest(p: &Path) -> Option<Contest> {
fn get_all_contest_info(task_dir: &str) -> Vec<Contest> {
fn walk_me_recursively(p: &Path, contests: &mut Vec<Contest>) {
if let Ok(paths) = std::fs::read_dir(p) {
print!("…");
use std::io::Write;
std::io::stdout().flush().unwrap();
let mut paths: Vec<_> = paths.filter_map(|r| r.ok()).collect();
paths.sort_by_key(|dir| dir.path());
for path in paths {
let p = path.unwrap().path();
let p = path.path();
walk_me_recursively(&p, contests);
}
}
......
......@@ -441,12 +441,13 @@ fn contests<C>(req: &mut Request) -> IronResult<Response>
fn contest<C>(req: &mut Request) -> IronResult<Response>
where C: MedalConnection + std::marker::Send + 'static {
let contest_id = req.expect_int::<i32>("contestid")?;
let secret = req.get_str("secret");
let session_token = req.require_session_token()?;
let query_string = req.url.query().map(|s| s.to_string());
let config = req.get::<Read<SharedConfiguration>>().unwrap();
let (template, data) =
with_conn![core::show_contest, C, req, contest_id, &session_token, query_string, login_info(&config)].aug(req)?;
with_conn![core::show_contest, C, req, contest_id, &session_token, query_string, login_info(&config), secret].aug(req)?;
let mut resp = Response::new();
resp.set_mut(Template::new(&template, data)).set_mut(status::Ok);
......@@ -514,13 +515,14 @@ fn contest_post<C>(req: &mut Request) -> IronResult<Response>
let contest_id = req.expect_int::<i32>("contestid")?;
let session_token = req.expect_session_token()?;
let csrf_token = {
let (csrf_token, secret) = {
let formdata = itry!(req.get_ref::<UrlEncodedBody>());
iexpect!(formdata.get("csrf_token"))[0].to_owned()
(iexpect!(formdata.get("csrf_token"))[0].to_owned(),
formdata.get("secret").map(|x| x[0].to_owned()))
};
// TODO: Was mit dem Result?
with_conn![core::start_contest, C, req, contest_id, &session_token, &csrf_token].aug(req)?;
with_conn![core::start_contest, C, req, contest_id, &session_token, &csrf_token, secret].aug(req)?;
Ok(Response::with((status::Found, Redirect(url_for!(req, "contest", "contestid" => format!("{}",contest_id))))))
}
......@@ -703,11 +705,17 @@ fn task<C>(req: &mut Request) -> IronResult<Response>
let task_id = req.expect_int::<i32>("taskid")?;
let session_token = req.require_session_token()?;
let (template, data) = with_conn![core::show_task, C, req, task_id, &session_token].aug(req)?;
let mut resp = Response::new();
resp.set_mut(Template::new(&template, data)).set_mut(status::Ok);
Ok(resp)
match with_conn![core::show_task, C, req, task_id, &session_token] {
Ok((template, data)) => {
let mut resp = Response::new();
resp.set_mut(Template::new(&template, data)).set_mut(status::Ok);
Ok(resp)
},
Err(contest_id) => {
// Idea: Append task, and if contest can be started immediately, we can just redirect again!
Ok(Response::with((status::Found, Redirect(url_for!(req, "contest", "contestid" => format!("{}",contest_id))))))
}
}
}
fn groups<C>(req: &mut Request) -> IronResult<Response>
......@@ -1175,7 +1183,7 @@ struct OAuthAccess {
#[allow(non_snake_case)]
#[serde(untagged)]
pub enum SchoolIdOrSchoolIds {
None(i32),
None(i32), // PMS sends -1 here if user has no schools associated (admin, jury)
SchoolId(String),
SchoolIds(Vec<String>),
}
......@@ -1213,7 +1221,7 @@ fn pms_hash_school(school_id: &str, secret: &str) -> String {
format!("{:02X?}", hashed_string).chars().filter(|c| c.is_ascii_alphanumeric()).collect()
}
fn oauth_pms(req: &mut Request, oauth_provider: OauthProvider, school_id: Option<&String>)
fn oauth_pms(req: &mut Request, oauth_provider: OauthProvider, selected_school_id: Option<&String>)
-> Result<Result<core::ForeignUserData, Response>, core::MedalError> {
use core::{UserSex, UserType};
use params::{Params, Value};
......@@ -1250,18 +1258,44 @@ fn oauth_pms(req: &mut Request, oauth_provider: OauthProvider, school_id: Option
// Unify ambiguous fields
user_data.userId = user_data.userID.or(user_data.userId);
let user_type = match user_data.userType.as_ref() {
"a" | "A" => UserType::Admin,
"t" | "T" => UserType::Teacher,
"s" | "S" => UserType::User,
_ => UserType::User,
};
let user_sex = match user_data.gender.as_ref() {
"m" | "M" => UserSex::Male,
"f" | "F" | "w" | "W" => UserSex::Female,
"?" => UserSex::Unknown,
_ => UserSex::Unknown,
};
match (&user_data.schoolId, user_type) {
// Students cannot have a list of schools
(Some(SchoolIdOrSchoolIds::SchoolIds(_)), UserType::User) => return e("#70"),
// If we need to make sure, a student has a school, we should add the case None(_) here
// Teachers must have a list of schools
(Some(SchoolIdOrSchoolIds::SchoolId(_)), UserType::Teacher) => return e("#71"),
(Some(SchoolIdOrSchoolIds::None(_)), UserType::Teacher) => return e("#72"),
// For other users, we currently don't care
_ => (),
}
// Does the user has an array of school (i.e. is he a teacher)?
if let Some(SchoolIdOrSchoolIds::SchoolIds(school_ids)) = user_data.schoolId {
// Has there been a school selected?
if let Some(school_id) = school_id {
if school_id == "none" && oauth_provider.allow_teacher_login_without_school == Some(true) {
if let Some(selected_school_id) = selected_school_id {
if selected_school_id == "none" && oauth_provider.allow_teacher_login_without_school == Some(true) {
// Nothing to do
}
// Is the school a valid school for the user?
else if school_ids.contains(&school_id) {
else if school_ids.contains(&selected_school_id) {
if let Some(mut user_id) = user_data.userId {
user_id.push('/');
user_id.push_str(&school_id);
user_id.push_str(&selected_school_id);
user_data.userId = Some(user_id);
}
} else {
......@@ -1309,24 +1343,14 @@ fn oauth_pms(req: &mut Request, oauth_provider: OauthProvider, school_id: Option
return Err(core::MedalError::ConfigurationError);
}
}
} else if school_id.is_some() {
} else if selected_school_id.is_some() {
// A school has apparently been selected but the user is actually not a teacher
return e("#50");
}
Ok(Ok(core::ForeignUserData { foreign_id: user_data.userId.ok_or(er("#60"))?,
foreign_type: match user_data.userType.as_ref() {
"a" | "A" => UserType::Admin,
"t" | "T" => UserType::Teacher,
"s" | "S" => UserType::User,
_ => UserType::User,
},
sex: match user_data.gender.as_ref() {
"m" | "M" => UserSex::Male,
"f" | "F" | "w" | "W" => UserSex::Female,
"?" => UserSex::Unknown,
_ => UserSex::Unknown,
},
foreign_type: user_type,
sex: user_sex,
firstname: user_data.firstName,
lastname: user_data.lastName }))
}
......@@ -1404,9 +1428,11 @@ pub fn start_server<C>(conn: C, config: Config) -> iron::error::HttpResult<iron:
greet: get "/" => greet_personal::<C>,
contests: get "/contest/" => contests::<C>,
contest: get "/contest/:contestid" => contest::<C>,
contest_secret: get "/contest/:contestid/:secret" => contest::<C>,
contestresults: get "/contest/:contestid/result/" => contestresults::<C>,
contestresults_download: get "/contest/:contestid/result/download" => contestresults_download::<C>,
contest_post: post "/contest/:contestid" => contest_post::<C>,
contest_post_secret: post "/contest/:contestid/:secret" => contest_post::<C>, // just ignoring the secret
login: get "/login" => login::<C>,
login_post: post "/login" => login_post::<C>,
login_code_post: post "/clogin" => login_code_post::<C>,
......
......@@ -14,17 +14,17 @@ function hash_to_dict() {
window.hashdict = hash_to_dict();
window.load_task_object = function (callback) {
window.load_task_object = function (callback, error) {
console.log(callback);
$.get("/load/" + window.hashdict["taskid"], {},
function(data) {
callback(data);
}, "json").fail(function(){
alert("Load failed.");
if (error) { error(); } else { alert("Load failed."); }
})
}
window.save_task_object = function (object, grade, callback) {
window.save_task_object = function (object, grade, callback, error) {
if (!grade) grade = 0;
if (!callback) callback = function(data){};
......@@ -34,12 +34,12 @@ window.save_task_object = function (object, grade, callback) {
grade: JSON.stringify(grade)
}
$.post("/save/" + window.hashdict["taskid"], params, callback, "json").fail(function(){
alert("Save failed.");
if (error) { error(); } else { alert("Save failed."); }
});
}
window.load_subtask_object = function (subtaskname, callback) {
window.load_subtask_object = function (subtaskname, callback, error) {
var params = {
subtask: subtaskname
}
......@@ -47,11 +47,11 @@ window.load_subtask_object = function (subtaskname, callback) {
function(data) {
callback(data);
}, "json").fail(function(){
alert("Load failed.");
if (error) { error(); } else { alert("Load failed."); }
})
}
window.save_subtask_object = function (subtaskname, object, grade, callback) {
window.save_subtask_object = function (subtaskname, object, grade, callback, error) {
if (!grade) grade = 0;
if (!callback) callback = function(data){};
......@@ -62,6 +62,6 @@ window.save_subtask_object = function (subtaskname, object, grade, callback) {
grade: JSON.stringify(grade)
}
$.post("/save/" + window.hashdict["taskid"], params, callback, "json").fail(function(){
alert("Save failed.");
if (error) { error(); } else { alert("Save failed."); }
});
}
......@@ -20,7 +20,7 @@ var options = {
noScore:0,
randomSeed:0,
readOnly:false,
options:{difficulty:"easy"},
options:{difficulty:"easy", log:1},
}
var myLoadViews = {
......@@ -59,8 +59,12 @@ function getTaskProxyCallback(task) {
task.reloadAnswer('', reloadAnswerCallback, ec("task.reloadAnswer"));
}
}
function load_task_error() {
task.reloadAnswer('', reloadAnswerCallback, ec("task.reloadAnswer"));
alert("Laden fehlgeschlagen");
}
window.load_task_object(load_task_callback);
window.load_task_object(load_task_callback, load_task_error);
}
function showViewsCallback(){
......@@ -75,16 +79,30 @@ function getTaskProxyCallback(task) {
task.load(myLoadViews, loadCallback, ec("task.load"))
}
var previous_answer = "";
function getAnswerCallback(answer) {
// If the answer did not change since last save, there is nothing to do
if (answer == previous_answer) {
return;
}
console.log("In task.gradeAnswer callback:");
console.log(answer);
function gradeAnswerCallback(score, message, scoreToken){
function save_task_callback() {
console.log("OK transmission");
// Now we know that the answer has been saved
previous_answer = answer;
};
function save_task_error() {
console.log("ERROR transmission");
alert("Speichern fehlgeschlagen");
};
window.save_task_object({"text": answer}, score, save_task_callback)
window.save_task_object({"text": answer}, score, save_task_callback, save_task_error);
}
task.gradeAnswer(answer, {}, gradeAnswerCallback, ec("task.gradeAnswer"));
......@@ -107,6 +125,10 @@ function getTaskProxyCallback(task) {
else if (mode == 'next' || mode == 'nextImmediate') {
window.parent.redirectOverview();
}
if (mode == 'log') {
task.getAnswer(getAnswerCallback, ec("task.getAnswer"));
if (cb) {cb();}
}
else {
console.error("Unknown mode: '" + mode + "'");
if (ecb) {ecb();}
......@@ -132,6 +154,10 @@ function getTaskProxyCallback(task) {
TaskProxyManager.setPlatform(task, platform);
task.getViews(getViewsCallback, ec("task.getViews"));
setInterval(function(){
task.getAnswer(getAnswerCallback, ec("task.getAnswer"));
}, 10000);
}
function main() {
......
......@@ -20,7 +20,7 @@ var options = {
noScore:0,
randomSeed:0,
readOnly:false,
options:{difficulty:"easy"},
options:{difficulty:"easy", log:1},
}
var myLoadViews = {
......@@ -59,8 +59,12 @@ function getTaskProxyCallback(task) {
task.reloadAnswer('', reloadAnswerCallback, ec("task.reloadAnswer"));
}
}
function load_task_error() {
task.reloadAnswer('', reloadAnswerCallback, ec("task.reloadAnswer"));
alert("Laden fehlgeschlagen");
}
window.load_task_object(load_task_callback);
window.load_task_object(load_task_callback, load_task_error);
}
function showViewsCallback(){
......@@ -75,16 +79,30 @@ function getTaskProxyCallback(task) {
task.load(myLoadViews, loadCallback, ec("task.load"))
}
var previous_answer = "";
function getAnswerCallback(answer) {
// If the answer did not change since last save, there is nothing to do
if (answer == previous_answer) {
return;
}
console.log("In task.gradeAnswer callback:");
console.log(answer);
function gradeAnswerCallback(score, message, scoreToken){
function save_task_callback() {
console.log("OK transmission");
// Now we know that the answer has been saved
previous_answer = answer;
};
function save_task_error() {
console.log("ERROR transmission");
alert("Speichern fehlgeschlagen");
};
window.save_task_object({"text": answer}, score, save_task_callback)
window.save_task_object({"text": answer}, score, save_task_callback, save_task_error);
}
task.gradeAnswer(answer, {}, gradeAnswerCallback, ec("task.gradeAnswer"));
......@@ -107,6 +125,10 @@ function getTaskProxyCallback(task) {
else if (mode == 'next' || mode == 'nextImmediate') {
window.parent.redirectOverview();
}
if (mode == 'log') {
task.getAnswer(getAnswerCallback, ec("task.getAnswer"));
if (cb) {cb();}
}
else {
console.error("Unknown mode: '" + mode + "'");
if (ecb) {ecb();}
......@@ -132,6 +154,10 @@ function getTaskProxyCallback(task) {
TaskProxyManager.setPlatform(task, platform);
task.getViews(getViewsCallback, ec("task.getViews"));
setInterval(function(){
task.getAnswer(getAnswerCallback, ec("task.getAnswer"));
}, 10000);
}
function main() {
......
......@@ -11,7 +11,9 @@
<link rel="icon" href="/static/images/favicon.png" type="image/png">
<style>
body{
margin: 0px;
margin: 0px;
padding: 0px;
overflow: clip;
font-family:sans-serif;
}
......@@ -22,8 +24,8 @@ padding: 8px;
margin:0px;
background: #8ca405;
color:white;
font-size:14pt;
height: 40px;
font-size:12pt;
height: 35px;
overflow-y:hidden;
}
......@@ -64,11 +66,12 @@ padding-right: 20px;
#bar>div.highlight {
background: #f5fbe8;
color:#334900;
color:#334900;
font-size: 10pt;
}
iframe {
width: 100vw;border: 0px;height: calc(100vh - 45px);
width: 100vw;border: 0px;height: calc(100vh - 35px);
}
a {
......
......@@ -18,6 +18,9 @@
padding: 0px;
margin: -20px;
}
.hoverfade:hover {
filter: brightness(95%) saturate(110%);
}
</style>
</head>
<body style="background-color: {{#if config.server_message}}#fbebeb{{else}}white{{/if}};">
......
......@@ -108,7 +108,9 @@
<p>
<form action="" method="post">
<input type="hidden" name="csrf_token" value="{{csrf_token}}">
<input class="button is-warning is-medium" type="submit" value="{{#if contest.duration}}⏱ &nbsp; {{/if}}Jetzt starten! {{#if contest.duration}}({{contest.duration}} min){{/if}}" style="font-weight:bold;">
{{#if secret_field}}<input {{#if secret_field_prefill}}type="hidden"{{/if}} name="secret" value="{{secret_field_prefill}}" placeholder="Wettbewerbspasswort">{{#if secret_field_prefill}}{{else}}<br>&nbsp;<br>{{/if}}{{/if}}
{{#if secret_wrong}}Das eingegeben Passwort ist nicht korrekt.{{/if}}
<input class="button is-warning is-medium" type="submit" value="{{#if contest.duration}}⏱ &nbsp; {{/if}}{{#if secret_field_prefill}}{{else}}🔒 &nbsp;{{/if}}Jetzt starten! {{#if contest.duration}}({{contest.duration}} min){{/if}}" style="font-weight:bold;">
</form>
</p>
......
{{#*inline "page"}}
{{#if config.notification}}
<div class="columns">
<div class="column is-8 is-offset-2">