core.rs 53.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
/*  medal                                                                                                            *\
 *  Copyright (C) 2020  Bundesweite Informatikwettbewerbe                                                            *
 *                                                                                                                   *
 *  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   *
\*  <http://www.gnu.org/licenses/>.                                                                                  */

15 16
use time;

Robert Czechowski's avatar
Robert Czechowski committed
17
use db_conn::MedalConnection;
18
use db_objects::OptionSession;
19
use db_objects::SessionUser;
20
use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
21 22 23
use helpers;
use oauth_provider::OauthProvider;
use webfw_iron::{json_val, to_json};
24

25 26
#[derive(Serialize, Deserialize)]
pub struct SubTaskInfo {
27
    pub id: i32,
28
    pub linktext: String,
29 30
    pub active: bool,
    pub greyout: bool,
31 32 33 34 35 36 37 38
}

#[derive(Serialize, Deserialize)]
pub struct TaskInfo {
    pub name: String,
    pub subtasks: Vec<SubTaskInfo>,
}

Robert Czechowski's avatar
Robert Czechowski committed
39 40
#[derive(Serialize, Deserialize)]
pub struct ContestInfo {
41
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
42
    pub name: String,
43
    pub duration: i32,
Robert Czechowski's avatar
Robert Czechowski committed
44 45 46
    pub public: bool,
}

47 48
#[derive(Clone)]
pub enum MedalError {
49 50
    NotLoggedIn,
    AccessDenied,
51 52 53
    CsrfCheckFailed,
    SessionTimeout,
    DatabaseError,
54
    PasswordHashingError,
55
    UnmatchedPasswords,
56
}
57

58 59 60 61
type MedalValue = (String, json_val::Map<String, json_val::Value>);
type MedalResult<T> = Result<T, MedalError>;
type MedalValueResult = MedalResult<MedalValue>;

62 63 64 65
fn fill_user_data(session: &SessionUser, data: &mut json_val::Map<String, serde_json::Value>) {
    if session.is_logged_in() {
        data.insert("logged_in".to_string(), to_json(&true));
    }
66 67 68
    if session.is_admin() {
        data.insert("admin".to_string(), to_json(&true));
    }
69 70 71 72 73
    data.insert("username".to_string(), to_json(&session.username));
    data.insert("firstname".to_string(), to_json(&session.firstname));
    data.insert("lastname".to_string(), to_json(&session.lastname));
    data.insert("teacher".to_string(), to_json(&session.is_teacher));
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
74 75 76
    data.insert("parent".to_string(), to_json(&"base"));
}

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
fn fill_oauth_data((self_url, oauth_providers): (Option<String>, Option<Vec<OauthProvider>>),
                   data: &mut json_val::Map<String, serde_json::Value>)
{
    let mut oauth_links: Vec<(String, String, String)> = Vec::new();
    if let Some(oauth_providers) = oauth_providers {
        for oauth_provider in oauth_providers {
            oauth_links.push((oauth_provider.provider_id.to_owned(),
                              oauth_provider.login_link_text.to_owned(),
                              oauth_provider.url.to_owned()));
        }
    }

    data.insert("self_url".to_string(), to_json(&self_url));
    data.insert("oauth_links".to_string(), to_json(&oauth_links));
}

93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
fn grade_to_string(grade: i32) -> String {
    match grade {
        0 => "Noch kein Schüler".to_string(),
        n @ 1..=10 => format!("{}", n),
        11 => "11 (G8)".to_string(),
        12 => "12 (G8)".to_string(),
        111 => "11 (G9)".to_string(),
        112 => "12 (G9)".to_string(),
        113 => "13 (G9)".to_string(),
        114 => "Berufsschule".to_string(),
        255 => "Kein Schüler mehr".to_string(),
        _ => "?".to_string(),
    }
}

Robert Czechowski's avatar
Robert Czechowski committed
108
pub fn index<T: MedalConnection>(conn: &T, session_token: Option<String>,
109
                                 oauth_infos: (Option<String>, Option<Vec<OauthProvider>>))
Robert Czechowski's avatar
Robert Czechowski committed
110 111
                                 -> (String, json_val::Map<String, json_val::Value>)
{
112 113 114 115 116 117
    let mut data = json_val::Map::new();

    //let mut contests = Vec::new();

    if let Some(token) = session_token {
        if let Some(session) = conn.get_session(&token) {
118
            fill_user_data(&session, &mut data);
119 120 121
        }
    }

122
    fill_oauth_data(oauth_infos, &mut data);
123

124
    data.insert("parent".to_string(), to_json(&"base"));
125 126 127
    ("index".to_owned(), data)
}

128
pub fn status<T: MedalConnection>(conn: &T, _: ()) -> String { conn.get_debug_information() }
129

130 131 132 133 134 135 136
pub fn debug<T: MedalConnection>(conn: &T, session_token: Option<String>)
                                 -> (String, json_val::Map<String, json_val::Value>) {
    let mut data = json_val::Map::new();

    if let Some(token) = session_token {
        if let Some(session) = conn.get_session(&token) {
            data.insert("known_session".to_string(), to_json(&true));
137
            data.insert("session_id".to_string(), to_json(&session.id));
138 139 140 141 142 143 144 145 146 147 148 149 150
            data.insert("now_timestamp".to_string(), to_json(&time::get_time().sec));
            if let Some(last_activity) = session.last_activity {
                data.insert("session_timestamp".to_string(), to_json(&last_activity.sec));
                data.insert("timediff".to_string(), to_json(&(time::get_time() - last_activity).num_seconds()));
            }
            if session.is_alive() {
                data.insert("alive_session".to_string(), to_json(&true));
                if session.is_logged_in() {
                    data.insert("logged_in".to_string(), to_json(&true));
                    data.insert("username".to_string(), to_json(&session.username));
                    data.insert("firstname".to_string(), to_json(&session.firstname));
                    data.insert("lastname".to_string(), to_json(&session.lastname));
                    data.insert("teacher".to_string(), to_json(&session.is_teacher));
151 152
                    data.insert("oauth_provider".to_string(), to_json(&session.oauth_provider));
                    data.insert("oauth_id".to_string(), to_json(&session.oauth_foreign_id));
153 154
                    data.insert("logincode".to_string(), to_json(&session.logincode));
                    data.insert("managed_by".to_string(), to_json(&session.managed_by));
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
                }
            }
        }
        data.insert("session".to_string(), to_json(&token));
    } else {
        data.insert("session".to_string(), to_json(&"No session token given"));
    }

    ("debug".to_owned(), data)
}

pub fn debug_create_session<T: MedalConnection>(conn: &T, session_token: Option<String>) {
    if let Some(token) = session_token {
        conn.get_session_or_new(&token);
    }
}

172 173 174 175
#[derive(PartialEq, Eq)]
pub enum ContestVisibility {
    All,
    Open,
176
    Current,
177
    LoginRequired,
178 179
}

180
pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str,
181
                                         oauth_infos: (Option<String>, Option<Vec<OauthProvider>>),
182 183 184
                                         visibility: ContestVisibility)
                                         -> MedalValue
{
Robert Czechowski's avatar
Robert Czechowski committed
185 186
    let mut data = json_val::Map::new();

187
    let session = conn.get_session_or_new(&session_token);
188
    fill_user_data(&session, &mut data);
189

190 191 192 193
    if session.is_logged_in() {
        data.insert("can_start".to_string(), to_json(&true));
    }

194
    fill_oauth_data(oauth_infos, &mut data);
195

196 197 198 199 200 201 202 203 204
    let now = time::get_time();
    let v: Vec<ContestInfo> =
        conn.get_contest_list()
            .iter()
            .filter(|c| c.public)
            .filter(|c| c.end.map(|end| now <= end).unwrap_or(true) || visibility == ContestVisibility::All)
            .filter(|c| c.duration == 0 || visibility != ContestVisibility::Open)
            .filter(|c| c.duration != 0 || visibility != ContestVisibility::Current)
            .filter(|c| c.requires_login.unwrap_or(false) || visibility != ContestVisibility::LoginRequired)
205 206 207 208 209
            .filter(|c| {
                !c.requires_login.unwrap_or(false)
                || visibility == ContestVisibility::LoginRequired
                || visibility == ContestVisibility::All
            })
210 211 212
            .map(|c| ContestInfo { id: c.id.unwrap(), name: c.name.clone(), duration: c.duration, public: c.public })
            .collect();

Robert Czechowski's avatar
Robert Czechowski committed
213
    data.insert("contest".to_string(), to_json(&v));
214 215
    data.insert("contestlist_header".to_string(),
                to_json(&match visibility {
216
                            ContestVisibility::Open => "Trainingsaufgaben",
217
                            ContestVisibility::Current => "Aktuelle Wettbewerbe",
218
                            ContestVisibility::LoginRequired => "Herausforderungen",
219 220
                            ContestVisibility::All => "Alle Wettbewerbe",
                        }));
Robert Czechowski's avatar
Robert Czechowski committed
221 222 223 224

    ("contests".to_owned(), data)
}

225
fn generate_subtaskstars(tg: &Taskgroup, grade: &Grade, ast: Option<i32>) -> Vec<SubTaskInfo> {
226 227
    let mut subtaskinfos = Vec::new();
    let mut not_print_yet = true;
228
    for st in &tg.tasks {
229 230 231 232 233 234
        let mut blackstars: usize = 0;
        if not_print_yet && st.stars >= grade.grade.unwrap_or(0) {
            blackstars = grade.grade.unwrap_or(0) as usize;
            not_print_yet = false;
        }

235 236 237 238 239 240
        let greyout = not_print_yet && st.stars < grade.grade.unwrap_or(0);
        let active = ast.is_some() && st.id == ast;
        let linktext = format!("{}{}",
                               str::repeat("★", blackstars as usize),
                               str::repeat("☆", st.stars as usize - blackstars as usize));
        let si = SubTaskInfo { id: st.id.unwrap(), linktext: linktext, active, greyout };
241 242 243 244 245 246

        subtaskinfos.push(si);
    }
    subtaskinfos
}

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
#[derive(Serialize, Deserialize)]
pub struct ContestStartConstraints {
    pub contest_not_begun: bool,
    pub contest_over: bool,
    pub contest_running: bool,
    pub grade_too_low: bool,
    pub grade_too_high: bool,
    pub grade_matching: bool,
}

fn check_contest_constraints(session: &SessionUser, contest: &Contest) -> ContestStartConstraints {
    let now = time::get_time();
    let student_grade = session.grade % 100 - if session.grade / 100 == 1 { 1 } else { 0 };

    let contest_not_begun = contest.start.map(|start| now < start).unwrap_or(false);
    let contest_over = contest.end.map(|end| now > end).unwrap_or(false);
    let grade_too_low =
        contest.min_grade.map(|min_grade| student_grade < min_grade && !session.is_teacher).unwrap_or(false);
    let grade_too_high =
        contest.max_grade.map(|max_grade| student_grade > max_grade && !session.is_teacher).unwrap_or(false);

    let contest_running = !contest_not_begun && !contest_over;
    let grade_matching = !grade_too_low && !grade_too_high;

    ContestStartConstraints { contest_not_begun,
                              contest_over,
                              contest_running,
                              grade_too_low,
                              grade_too_high,
                              grade_matching }
}

279 280 281 282 283
pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
                                        query_string: Option<String>,
                                        oauth_infos: (Option<String>, Option<Vec<OauthProvider>>))
                                        -> MedalValueResult
{
284 285
    let session = conn.get_session_or_new(&session_token);

286
    let contest = conn.get_contest_by_id_complete(contest_id);
287
    let grades = conn.get_contest_user_grades(&session_token, contest_id);
288

289
    let mut opt_part = conn.get_participation(&session_token, contest_id);
290

291 292 293
    let ci = ContestInfo { id: contest.id.unwrap(),
                           name: contest.name.clone(),
                           duration: contest.duration,
294
                           public: contest.public };
Robert Czechowski's avatar
Robert Czechowski committed
295 296

    let mut data = json_val::Map::new();
297 298
    data.insert("parent".to_string(), to_json(&"base"));
    data.insert("empty".to_string(), to_json(&"empty"));
Robert Czechowski's avatar
Robert Czechowski committed
299
    data.insert("contest".to_string(), to_json(&ci));
300
    fill_oauth_data(oauth_infos, &mut data);
Robert Czechowski's avatar
Robert Czechowski committed
301

302
    let constraints = check_contest_constraints(&session, &contest);
303

304
    let can_start = session.is_logged_in() && constraints.contest_running && constraints.grade_matching;
305
    let has_duration = contest.duration > 0;
306

307
    data.insert("constraints".to_string(), to_json(&constraints));
308
    data.insert("has_duration".to_string(), to_json(&has_duration));
309
    data.insert("can_start".to_string(), to_json(&can_start));
310

311 312 313 314
    // Autostart if appropriate
    // TODO: Should participation start automatically for teacher? Even before the contest start?
    // Should teachers have all time access or only the same limited amount of time?
    // if opt_part.is_none() && (contest.duration == 0 || session.is_teacher) {
315 316 317 318 319 320
    if opt_part.is_none()
       && contest.duration == 0
       && constraints.contest_running
       && constraints.grade_matching
       && contest.requires_login != Some(true)
    {
321 322 323 324
        conn.new_participation(&session_token, contest_id).map_err(|_| MedalError::AccessDenied)?;
        opt_part = Some(Participation { contest: contest_id, user: session.id, start: time::get_time() });
    }

325
    let now = time::get_time();
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
    if let Some(start) = contest.start {
        if now < start {
            let until = start - now;
            data.insert("time_until_start".to_string(),
                        to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
        }
    }

    if let Some(end) = contest.end {
        if now < end {
            let until = end - now;
            data.insert("time_until_end".to_string(),
                        to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
        }
    }

    if session.is_logged_in() {
        data.insert("logged_in".to_string(), to_json(&true));
        data.insert("username".to_string(), to_json(&session.username));
        data.insert("firstname".to_string(), to_json(&session.firstname));
        data.insert("lastname".to_string(), to_json(&session.lastname));
        data.insert("teacher".to_string(), to_json(&session.is_teacher));
        data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
349 350
    }

351 352 353
    if let Some(participation) = opt_part {
        let mut totalgrade = 0;
        let mut max_totalgrade = 0;
354

355
        let mut tasks = Vec::new();
356
        for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
357 358 359 360 361 362 363
            let subtaskstars = generate_subtaskstars(&taskgroup, &grade, None);
            let ti = TaskInfo { name: taskgroup.name, subtasks: subtaskstars };
            tasks.push(ti);

            totalgrade += grade.grade.unwrap_or(0);
            max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
        }
364
        let relative_points = (totalgrade * 100) / max_totalgrade;
365 366

        data.insert("tasks".to_string(), to_json(&tasks));
Robert Czechowski's avatar
Robert Czechowski committed
367

368 369 370 371 372
        let now = time::get_time();
        let passed_secs = now.sec - participation.start.sec;
        if passed_secs < 0 {
            // behandle inkonsistente Serverzeit
        }
373 374
        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
        let is_time_left = contest.duration == 0 || left_secs >= 0;
375 376 377 378
        let is_time_up = !is_time_left;
        let left_min = left_secs / 60;
        let left_sec = left_secs % 60;
        let time_left = format!("{}:{:02}", left_min, left_sec);
379

380
        data.insert("is_started".to_string(), to_json(&true));
381
        data.insert("participation_start_date".to_string(), to_json(&passed_secs));
382 383
        data.insert("total_points".to_string(), to_json(&totalgrade));
        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
384 385 386
        data.insert("relative_points".to_string(), to_json(&relative_points));
        data.insert("is_time_left".to_string(), to_json(&is_time_left));
        data.insert("is_time_up".to_string(), to_json(&is_time_up));
387 388 389 390 391 392
        if is_time_left {
            data.insert("left_min".to_string(), to_json(&left_min));
            data.insert("left_sec".to_string(), to_json(&left_sec));
            data.insert("time_left".to_string(), to_json(&time_left));
            data.insert("seconds_left".to_string(), to_json(&left_secs));
        }
Robert Czechowski's avatar
Robert Czechowski committed
393
    }
394

395 396 397 398 399 400
    // This only checks if a query string is existent, so any query string will
    // lead to the assumption that a bare page is requested. This is useful to
    // disable caching (via random token) but should be changed if query string
    // can obtain more than only this meaning in the future
    if query_string.is_none() {
        data.insert("not_bare".to_string(), to_json(&true));
401 402
    }

403
    Ok(("contest".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
404 405
}

406
pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
407
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
408 409
    let mut data = json_val::Map::new();
    fill_user_data(&session, &mut data);
410

411 412
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

413
    let mut results: Vec<(String, i32, Vec<(String, String, i32, String, Vec<String>)>)> = Vec::new();
414 415

    for (group, groupdata) in resultdata {
416
        let mut groupresults: Vec<(String, String, i32, String, Vec<String>)> = Vec::new();
417

Daniel Brüning's avatar
Daniel Brüning committed
418
        //TODO: use user
419
        for (user, userdata) in groupdata {
420 421
            let mut userresults: Vec<String> = Vec::new();

422 423 424
            userresults.push(String::new());
            let mut summe = 0;

425
            for grade in userdata {
426 427 428 429 430
                if let Some(g) = grade.grade {
                    userresults.push(format!("{}", g));
                    summe += g;
                } else {
                    userresults.push(format!("–"));
431
                }
432 433
            }

434 435
            userresults[0] = format!("{}", summe);

436 437
            groupresults.push((user.firstname.unwrap_or_else(|| "–".to_string()),
                               user.lastname.unwrap_or_else(|| "–".to_string()),
438
                               user.id,
439
                               grade_to_string(user.grade),
440
                               userresults))
441
        }
442

443
        results.push((format!("{}", group.name), group.id.unwrap_or(0), groupresults));
444 445 446 447
    }

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
448 449

    let c = conn.get_contest_by_id(contest_id);
450 451
    let ci = ContestInfo { id: c.id.unwrap(), name: c.name.clone(), duration: c.duration, public: c.public };

452
    data.insert("contest".to_string(), to_json(&ci));
453
    data.insert("contestname".to_string(), to_json(&c.name));
454

455 456 457
    Ok(("contestresults".to_owned(), data))
}

458
pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str)
Robert Czechowski's avatar
Robert Czechowski committed
459
                                         -> MedalResult<()> {
460
    // TODO: Is _or_new the right semantic? We need a CSRF token anyway …
461
    let session = conn.get_session_or_new(&session_token);
462
    let contest = conn.get_contest_by_id(contest_id);
463

464
    // Check logged in or open contest
465
    if contest.duration != 0 && !session.is_logged_in() {
466
        return Err(MedalError::AccessDenied);
467
    }
468

469
    // Check CSRF token
470 471 472
    if session.is_logged_in() && session.csrf_token != csrf_token {
        return Err(MedalError::CsrfCheckFailed);
    }
473

474 475 476 477 478 479 480
    // Check other constraints
    let constraints = check_contest_constraints(&session, &contest);

    if !(constraints.contest_running && constraints.grade_matching) {
        return Err(MedalError::AccessDenied);
    }

481
    // Start contest
482
    match conn.new_participation(&session_token, contest_id) {
Robert Czechowski's avatar
Robert Czechowski committed
483
        Ok(_) => Ok(()),
484
        _ => Err(MedalError::AccessDenied), // Contest already started TODO: Maybe redirect to page with hint
Robert Czechowski's avatar
Robert Czechowski committed
485
    }
Robert Czechowski's avatar
Robert Czechowski committed
486 487
}

488
pub fn login<T: MedalConnection>(conn: &T, login_data: (String, String),
489
                                 oauth_infos: (Option<String>, Option<Vec<OauthProvider>>))
490 491
                                 -> Result<String, MedalValue>
{
Robert Czechowski's avatar
Robert Czechowski committed
492 493
    let (username, password) = login_data;

494
    match conn.login(None, &username, &password) {
Robert Czechowski's avatar
Robert Czechowski committed
495
        Ok(session_token) => Ok(session_token),
Robert Czechowski's avatar
Robert Czechowski committed
496 497
        Err(()) => {
            let mut data = json_val::Map::new();
498
            data.insert("reason".to_string(), to_json(&"Login fehlgeschlagen. Bitte erneut versuchen.".to_string()));
Robert Czechowski's avatar
Robert Czechowski committed
499
            data.insert("username".to_string(), to_json(&username));
500 501
            data.insert("parent".to_string(), to_json(&"base"));

502
            fill_oauth_data(oauth_infos, &mut data);
503

504
            Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
505 506 507 508
        }
    }
}

Robert Czechowski's avatar
Robert Czechowski committed
509
pub fn login_with_code<T: MedalConnection>(
510
    conn: &T, code: &str, oauth_infos: (Option<String>, Option<Vec<OauthProvider>>))
Robert Czechowski's avatar
Robert Czechowski committed
511
    -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
512
    match conn.login_with_code(None, &code) {
Robert Czechowski's avatar
Robert Czechowski committed
513 514 515 516 517 518 519
        Ok(session_token) => Ok(Ok(session_token)),
        Err(()) => match conn.create_user_with_groupcode(None, &code) {
            Ok(session_token) => Ok(Err(session_token)),
            Err(()) => {
                let mut data = json_val::Map::new();
                data.insert("reason".to_string(), to_json(&"Kein gültiger Code. Bitte erneut versuchen.".to_string()));
                data.insert("code".to_string(), to_json(&code));
520 521
                data.insert("parent".to_string(), to_json(&"base"));

522
                fill_oauth_data(oauth_infos, &mut data);
523

Robert Czechowski's avatar
Robert Czechowski committed
524
                Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
525
            }
Robert Czechowski's avatar
Robert Czechowski committed
526
        },
Robert Czechowski's avatar
Robert Czechowski committed
527 528 529
    }
}

530
pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
531
    session_token.map(|token| conn.logout(&token));
Robert Czechowski's avatar
Robert Czechowski committed
532 533
}

534
pub fn load_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, subtask: Option<String>)
Robert Czechowski's avatar
Robert Czechowski committed
535
                                           -> MedalResult<String> {
536
    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
537

538
    match match subtask {
Robert Czechowski's avatar
Robert Czechowski committed
539 540 541
              Some(s) => conn.load_submission(&session, task_id, Some(&s)),
              None => conn.load_submission(&session, task_id, None),
          } {
Robert Czechowski's avatar
Robert Czechowski committed
542
        Some(submission) => Ok(submission.value),
Robert Czechowski's avatar
Robert Czechowski committed
543
        None => Ok("{}".to_string()),
Robert Czechowski's avatar
Robert Czechowski committed
544 545 546
    }
}

547 548
pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, csrf_token: &str,
                                           data: String, grade: i32, subtask: Option<String>)
Robert Czechowski's avatar
Robert Czechowski committed
549 550
                                           -> MedalResult<String>
{
551
    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
552 553

    if session.csrf_token != csrf_token {
554
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
555 556
    }

557 558 559 560 561 562 563 564 565 566 567 568
    let (_, _, c) = conn.get_task_by_id_complete(task_id);

    match conn.get_participation(&session_token, c.id.expect("Value from database")) {
        None => return Err(MedalError::AccessDenied),
        Some(participation) => {
            let now = time::get_time();
            let passed_secs = now.sec - participation.start.sec;
            if passed_secs < 0 {
                // behandle inkonsistente Serverzeit
            }

            let left_secs = i64::from(c.duration) * 60 - passed_secs;
569
            if c.duration > 0 && left_secs < -10 {
570 571 572
                return Err(MedalError::AccessDenied);
                // Contest over
                // TODO: Nicer message!
573 574 575 576
            }
        }
    }

Robert Czechowski's avatar
Robert Czechowski committed
577 578 579 580 581 582 583 584 585 586
    let submission = Submission { id: None,
                                  session_user: session.id,
                                  task: task_id,
                                  grade: grade,
                                  validated: false,
                                  nonvalidated_grade: grade,
                                  needs_validation: true,
                                  subtask_identifier: subtask,
                                  value: data,
                                  date: time::get_time() };
587

Robert Czechowski's avatar
Robert Czechowski committed
588 589 590 591 592
    conn.submit_submission(submission);

    Ok("{}".to_string())
}

593
pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str) -> MedalValueResult {
594
    let session = conn.get_session_or_new(&session_token);
Robert Czechowski's avatar
Robert Czechowski committed
595

596
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
597 598
    let grade = conn.get_taskgroup_user_grade(&session_token, tg.id.unwrap()); // TODO: Unwrap?
    let tasklist = conn.get_contest_by_id_complete(c.id.unwrap()); // TODO: Unwrap?
599

600 601 602
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
603

604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
    let mut subtaskstars = Vec::new();

    for taskgroup in tasklist.taskgroups {
        if current_found {
            nexttaskgroup = Some(taskgroup);
            break;
        }

        if taskgroup.id == tg.id {
            current_found = true;
            subtaskstars = generate_subtaskstars(&taskgroup, &grade, Some(task_id));
        } else {
            prevtaskgroup = Some(taskgroup);
        }
    }
619

620
    match conn.get_participation(&session_token, c.id.expect("Value from database")) {
621
        None => Err(MedalError::AccessDenied),
622 623 624 625 626 627
        Some(participation) => {
            let now = time::get_time();
            let passed_secs = now.sec - participation.start.sec;
            if passed_secs < 0 {
                // behandle inkonsistente Serverzeit
            }
Robert Czechowski's avatar
Robert Czechowski committed
628

629
            let mut data = json_val::Map::new();
630
            data.insert("participation_start_date".to_string(), to_json(&format!("{}", passed_secs)));
631 632 633
            data.insert("subtasks".to_string(), to_json(&subtaskstars));
            data.insert("prevtask".to_string(), to_json(&prevtaskgroup.map(|tg| tg.tasks[0].id)));
            data.insert("nexttask".to_string(), to_json(&nexttaskgroup.map(|tg| tg.tasks[0].id))); // TODO: fail better
Robert Czechowski's avatar
Robert Czechowski committed
634

635
            let left_secs = i64::from(c.duration) * 60 - passed_secs;
636
            if c.duration > 0 && left_secs < 0 {
637
                Err(MedalError::AccessDenied)
638 639 640 641 642
            // Contest over
            // TODO: Nicer message!
            } else {
                let (hour, min, sec) = (left_secs / 3600, left_secs / 60 % 60, left_secs % 60);

Robert Czechowski's avatar
Robert Czechowski committed
643 644
                data.insert("time_left".to_string(), to_json(&format!("{}:{:02}", hour, min)));
                data.insert("time_left_sec".to_string(), to_json(&format!(":{:02}", sec)));
645 646

                let taskpath = format!("{}{}", c.location, t.location);
Robert Czechowski's avatar
Robert Czechowski committed
647

648
                data.insert("contestname".to_string(), to_json(&c.name));
649 650
                data.insert("name".to_string(), to_json(&tg.name));
                data.insert("taskid".to_string(), to_json(&task_id));
651
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
652 653
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
654
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
655

Robert Czechowski's avatar
Robert Czechowski committed
656 657 658 659
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

660 661 662 663
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
664
}
Robert Czechowski's avatar
Robert Czechowski committed
665

Robert Czechowski's avatar
Robert Czechowski committed
666 667
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
668
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
669 670 671 672 673
    pub name: String,
    pub tag: String,
    pub code: String,
}

Daniel Brüning's avatar
Daniel Brüning committed
674
pub fn show_groups<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
675
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
676

Robert Czechowski's avatar
Robert Czechowski committed
677
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
678 679

    let mut data = json_val::Map::new();
680
    fill_user_data(&session, &mut data);
Robert Czechowski's avatar
Robert Czechowski committed
681

Robert Czechowski's avatar
Robert Czechowski committed
682 683 684 685 686 687 688 689
    let v: Vec<GroupInfo> =
        conn.get_groups(session.id)
            .iter()
            .map(|g| GroupInfo { id: g.id.unwrap(),
                                 name: g.name.clone(),
                                 tag: g.tag.clone(),
                                 code: g.groupcode.clone() })
            .collect();
Robert Czechowski's avatar
Robert Czechowski committed
690
    data.insert("group".to_string(), to_json(&v));
691
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
692

693
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
694 695
}

Robert Czechowski's avatar
Robert Czechowski committed
696 697
#[derive(Serialize, Deserialize)]
pub struct MemberInfo {
698
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
699 700
    pub firstname: String,
    pub lastname: String,
701
    pub grade: String,
Robert Czechowski's avatar
Robert Czechowski committed
702 703 704
    pub logincode: String,
}

705
pub fn show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
706
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
707
    let group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
708

Robert Czechowski's avatar
Robert Czechowski committed
709
    let mut data = json_val::Map::new();
710
    fill_user_data(&session, &mut data);
711

Robert Czechowski's avatar
Robert Czechowski committed
712
    if group.admin != session.id {
713
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
714 715
    }

Robert Czechowski's avatar
Robert Czechowski committed
716 717 718 719 720 721 722 723 724 725 726
    let gi = GroupInfo { id: group.id.unwrap(),
                         name: group.name.clone(),
                         tag: group.tag.clone(),
                         code: group.groupcode.clone() };

    let v: Vec<MemberInfo> =
        group.members
             .iter()
             .map(|m| MemberInfo { id: m.id,
                                   firstname: m.firstname.clone().unwrap_or_else(|| "".to_string()),
                                   lastname: m.lastname.clone().unwrap_or_else(|| "".to_string()),
727
                                   grade: grade_to_string(m.grade),
Robert Czechowski's avatar
Robert Czechowski committed
728 729
                                   logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()) })
             .collect();
Robert Czechowski's avatar
Robert Czechowski committed
730 731 732

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
733
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
734 735 736

    Ok(("group".to_string(), data))
}
Robert Czechowski's avatar
Robert Czechowski committed
737

738
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
739 740
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
741

Daniel Brüning's avatar
Daniel Brüning committed
742
pub fn add_group<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, name: String, tag: String)
743
                                     -> MedalResult<i32> {
744
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::AccessDenied)?;
Robert Czechowski's avatar
Robert Czechowski committed
745 746

    if session.csrf_token != csrf_token {
747
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
748 749
    }

750
    let group_code = helpers::make_group_code();
751
    // TODO: check for collisions
752

Robert Czechowski's avatar
Robert Czechowski committed
753 754
    let mut group =
        Group { id: None, name: name, groupcode: group_code, tag: tag, admin: session.id, members: Vec::new() };
Robert Czechowski's avatar
Robert Czechowski committed
755

Robert Czechowski's avatar
Robert Czechowski committed
756
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
757 758 759

    Ok(group.id.unwrap())
}
760

761
pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
762
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
763 764 765 766 767 768 769

    let mut data = json_val::Map::new();
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));

    Ok(("groupcsv".to_string(), data))
}

770
// TODO: Should creating the users and groups happen in a batch operation to speed things up?
Robert Czechowski's avatar
rustfmt  
Robert Czechowski committed
771 772
pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
                                         -> MedalResult<()> {
773
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
774 775 776 777 778

    if session.csrf_token != csrf_token {
        return Err(MedalError::CsrfCheckFailed);
    }

Robert Czechowski's avatar
rustfmt  
Robert Czechowski committed
779 780
    println!("{}", group_data);

781 782 783 784 785
    let mut v: Vec<Vec<String>> = serde_json::from_str(group_data).or(Err(MedalError::AccessDenied))?; // TODO: Change error type
    v.sort_unstable_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());

    let mut group_code = "".to_string();
    let mut name = "".to_string();
Robert Czechowski's avatar
rustfmt  
Robert Czechowski committed
786 787 788 789 790 791 792
    let mut group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: "".to_string(),
                            admin: session.id,
                            members: Vec::new() };

793 794
    for line in v {
        if name != line[0] {
795
            if name != "" {
796
                conn.create_group_with_users(group);
Robert Czechowski's avatar
rustfmt  
Robert Czechowski committed
797
            }
798 799 800 801
            name = line[0].clone();
            group_code = helpers::make_group_code();
            // TODO: check for collisions

Robert Czechowski's avatar
rustfmt  
Robert Czechowski committed
802 803 804 805 806 807
            group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: name.clone(),
                            admin: session.id,
                            members: Vec::new() };
808 809 810 811 812 813 814 815 816 817 818
        }

        let mut user = SessionUser::group_user_stub();
        user.grade = line[1].parse::<i32>().unwrap_or(0);
        user.firstname = Some(line[2].clone());
        user.lastname = Some(line[3].clone());

        group.members.push(user);
    }
    conn.create_group_with_users(group);

819 820 821
    Ok(())
}

Daniel Brüning's avatar
Daniel Brüning committed
822
#[allow(dead_code)]
823
pub fn show_groups_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
824
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Daniel Brüning's avatar
Daniel Brüning committed
825 826
    //TODO: use g
    let _g = conn.get_contest_groups_grades(session.id, contest_id);
827

828
    let data = json_val::Map::new();
829 830 831

    Ok(("groupresults".into(), data))
}
Robert Czechowski's avatar
Robert Czechowski committed
832

833
pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
Robert Czechowski's avatar
Robert Czechowski committed
834 835 836
                                        query_string: Option<String>)
                                        -> MedalValueResult
{
837
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
838 839

    let mut data = json_val::Map::new();
840
    fill_user_data(&session, &mut data);
Robert Czechowski's avatar
Robert Czechowski committed
841

842 843
    match user_id {
        None => {
844 845 846 847 848
            data.insert("profile_firstname".to_string(), to_json(&session.firstname));
            data.insert("profile_lastname".to_string(), to_json(&session.lastname));
            data.insert("profile_street".to_string(), to_json(&session.street));
            data.insert("profile_zip".to_string(), to_json(&session.zip));
            data.insert("profile_city".to_string(), to_json(&session.city));
849
            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
850 851 852 853 854
            if let Some(sex) = session.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
855

856
            data.insert("profile_logincode".to_string(), to_json(&session.logincode));
857
            if session.password.is_some() {
858
                data.insert("profile_username".to_string(), to_json(&session.username));
859 860
            }
            if session.managed_by.is_none() {
861
                data.insert("profile_not_in_group".into(), to_json(&true));
862
            }
863 864 865
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
866 867
            data.insert("ownprofile".into(), to_json(&true));

868
            if let Some(query) = query_string {
869
                if query.starts_with("status=") {
870
                    let status: &str = &query[7..];
871
                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch", "firstlogin"].contains(&status) {
872 873
                        data.insert((status).to_string(), to_json(&true));
                    }
874 875
                }
            }
Robert Czechowski's avatar
Robert Czechowski committed
876
        }
877 878 879 880 881 882 883 884
        Some(user_id) => {
            // TODO: Add test to check if this access restriction works
            let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
            let group = opt_group.ok_or(MedalError::AccessDenied)?;
            if group.admin != session.id {
                return Err(MedalError::AccessDenied);
            }

885 886 887 888 889
            data.insert("profile_firstname".to_string(), to_json(&user.firstname));
            data.insert("profile_lastname".to_string(), to_json(&user.lastname));
            data.insert("profile_street".to_string(), to_json(&session.street));
            data.insert("profile_zip".to_string(), to_json(&session.zip));
            data.insert("profile_city".to_string(), to_json(&session.city));
890
            data.insert(format!("sel{}", user.grade), to_json(&"selected"));
891 892 893 894 895
            if let Some(sex) = user.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
896

897
            data.insert("profile_logincode".to_string(), to_json(&user.logincode));
898
            if user.username.is_some() {
899
                data.insert("profile_username".to_string(), to_json(&user.username));
900 901
            }
            if user.managed_by.is_none() {
902
                data.insert("profile_not_in_group".into(), to_json(&true));
903
            }
904 905 906
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
907
            data.insert("ownprofile".into(), to_json(&false));
908

909 910 911 912 913 914 915 916
            if let Some(query) = query_string {
                if query.starts_with("status=") {
                    let status: &str = &query[7..];
                    if ["NothingChanged", "DataChanged"