core.rs 57 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
62
63
pub struct LoginInfo {
    pub password_login: bool,
    pub self_url: Option<String>,
    pub oauth_providers: Option<Vec<OauthProvider>>,
}

64
65
66
67
type MedalValue = (String, json_val::Map<String, json_val::Value>);
type MedalResult<T> = Result<T, MedalError>;
type MedalValueResult = MedalResult<MedalValue>;

68
69
70
71
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));
    }
72
73
74
    if session.is_admin() {
        data.insert("admin".to_string(), to_json(&true));
    }
75
76
77
78
79
    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));
80
    data.insert("parent".to_string(), to_json(&"base"));
81
82

    data.insert("medal_version".to_string(), to_json(&env!("CARGO_PKG_VERSION")));
83
84
}

85
fn fill_oauth_data(login_info: LoginInfo, data: &mut json_val::Map<String, serde_json::Value>) {
86
    let mut oauth_links: Vec<(String, String, String)> = Vec::new();
87
    if let Some(oauth_providers) = login_info.oauth_providers {
88
89
90
91
92
93
94
        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()));
        }
    }

95
    data.insert("self_url".to_string(), to_json(&login_info.self_url));
96
    data.insert("oauth_links".to_string(), to_json(&oauth_links));
97
98

    data.insert("password_login".to_string(), to_json(&login_info.password_login));
99
100
}

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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(),
    }
}

116
117
pub fn index<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo)
                                 -> (String, json_val::Map<String, json_val::Value>) {
118
119
120
121
    let mut data = json_val::Map::new();

    if let Some(token) = session_token {
        if let Some(session) = conn.get_session(&token) {
122
            fill_user_data(&session, &mut data);
123
124
125
        }
    }

126
    fill_oauth_data(login_info, &mut data);
127

128
    data.insert("parent".to_string(), to_json(&"base"));
129
130
131
    ("index".to_owned(), data)
}

132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
pub fn show_login<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo)
                                      -> (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) {
            fill_user_data(&session, &mut data);
        }
    }

    fill_oauth_data(login_info, &mut data);

    data.insert("parent".to_string(), to_json(&"base"));
    ("login".to_owned(), data)
}

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

150
151
152
153
154
155
156
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));
157
            data.insert("session_id".to_string(), to_json(&session.id));
158
159
160
161
162
163
164
165
166
167
168
169
170
            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));
171
172
                    data.insert("oauth_provider".to_string(), to_json(&session.oauth_provider));
                    data.insert("oauth_id".to_string(), to_json(&session.oauth_foreign_id));
173
174
                    data.insert("logincode".to_string(), to_json(&session.logincode));
                    data.insert("managed_by".to_string(), to_json(&session.managed_by));
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
                }
            }
        }
        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);
    }
}

192
193
194
195
#[derive(PartialEq, Eq)]
pub enum ContestVisibility {
    All,
    Open,
196
    Current,
197
    LoginRequired,
198
199
}

200
pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str, login_info: LoginInfo,
201
202
203
                                         visibility: ContestVisibility)
                                         -> MedalValue
{
Robert Czechowski's avatar
Robert Czechowski committed
204
205
    let mut data = json_val::Map::new();

206
    let session = conn.get_session_or_new(&session_token);
207
    fill_user_data(&session, &mut data);
208

209
210
211
212
    if session.is_logged_in() {
        data.insert("can_start".to_string(), to_json(&true));
    }

213
    fill_oauth_data(login_info, &mut data);
214

215
216
217
218
219
220
221
222
223
    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)
224
225
226
227
228
            .filter(|c| {
                !c.requires_login.unwrap_or(false)
                || visibility == ContestVisibility::LoginRequired
                || visibility == ContestVisibility::All
            })
229
230
231
            .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
232
    data.insert("contest".to_string(), to_json(&v));
233
234
    data.insert("contestlist_header".to_string(),
                to_json(&match visibility {
235
                            ContestVisibility::Open => "Trainingsaufgaben",
236
                            ContestVisibility::Current => "Aktuelle Wettbewerbe",
237
                            ContestVisibility::LoginRequired => "Herausforderungen",
238
239
                            ContestVisibility::All => "Alle Wettbewerbe",
                        }));
Robert Czechowski's avatar
Robert Czechowski committed
240
241
242
243

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

244
fn generate_subtaskstars(tg: &Taskgroup, grade: &Grade, ast: Option<i32>) -> Vec<SubTaskInfo> {
245
246
    let mut subtaskinfos = Vec::new();
    let mut not_print_yet = true;
247
    for st in &tg.tasks {
248
249
250
251
252
253
        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;
        }

254
255
256
257
258
        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));
Robert Czechowski's avatar
Robert Czechowski committed
259
        let si = SubTaskInfo { id: st.id.unwrap(), linktext, active, greyout };
260
261
262
263
264
265

        subtaskinfos.push(si);
    }
    subtaskinfos
}

266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#[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 }
}

298
pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
299
                                        query_string: Option<String>, login_info: LoginInfo)
300
301
                                        -> MedalValueResult
{
302
303
    let session = conn.get_session_or_new(&session_token);

304
    let contest = conn.get_contest_by_id_complete(contest_id);
305
    let grades = conn.get_contest_user_grades(&session_token, contest_id);
306

307
    let mut opt_part = conn.get_participation(&session_token, contest_id);
308

309
310
311
    let ci = ContestInfo { id: contest.id.unwrap(),
                           name: contest.name.clone(),
                           duration: contest.duration,
312
                           public: contest.public };
Robert Czechowski's avatar
Robert Czechowski committed
313
314

    let mut data = json_val::Map::new();
315
316
    data.insert("parent".to_string(), to_json(&"base"));
    data.insert("empty".to_string(), to_json(&"empty"));
Robert Czechowski's avatar
Robert Czechowski committed
317
    data.insert("contest".to_string(), to_json(&ci));
318
    data.insert("message".to_string(), to_json(&contest.message));
319
    fill_oauth_data(login_info, &mut data);
Robert Czechowski's avatar
Robert Czechowski committed
320

321
    let constraints = check_contest_constraints(&session, &contest);
322

323
    let can_start = session.is_logged_in() && constraints.contest_running && constraints.grade_matching;
324
    let has_duration = contest.duration > 0;
325

326
    data.insert("constraints".to_string(), to_json(&constraints));
327
    data.insert("has_duration".to_string(), to_json(&has_duration));
328
    data.insert("can_start".to_string(), to_json(&can_start));
329

330
331
332
333
    let has_tasks = contest.taskgroups.len() > 0;
    data.insert("has_tasks".to_string(), to_json(&has_tasks));
    data.insert("no_tasks".to_string(), to_json(&!has_tasks));

334
335
336
337
    // 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) {
338
339
340
341
342
343
    if opt_part.is_none()
       && contest.duration == 0
       && constraints.contest_running
       && constraints.grade_matching
       && contest.requires_login != Some(true)
    {
344
345
346
347
        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() });
    }

348
    let now = time::get_time();
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
    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));
372
373
    }

374
375
376
    if let Some(participation) = opt_part {
        let mut totalgrade = 0;
        let mut max_totalgrade = 0;
377

378
        let mut tasks = Vec::new();
379
        for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
380
381
382
383
384
385
386
            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);
        }
387
        let relative_points = (totalgrade * 100) / max_totalgrade;
388
389

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

391
392
393
394
395
        let now = time::get_time();
        let passed_secs = now.sec - participation.start.sec;
        if passed_secs < 0 {
            // behandle inkonsistente Serverzeit
        }
396
397
        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
        let is_time_left = contest.duration == 0 || left_secs >= 0;
398
399
400
401
        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);
402

403
        data.insert("is_started".to_string(), to_json(&true));
404
        data.insert("participation_start_date".to_string(), to_json(&passed_secs));
405
406
        data.insert("total_points".to_string(), to_json(&totalgrade));
        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
407
408
409
        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));
410
411
412
413
414
415
        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
416
    }
417

418
419
420
421
422
423
    // 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));
424
425
    }

426
    Ok(("contest".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
427
428
}

429
pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
430
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
431
432
    let mut data = json_val::Map::new();
    fill_user_data(&session, &mut data);
433

434
435
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

436
    let mut results: Vec<(String, i32, Vec<(String, String, i32, String, Vec<String>)>)> = Vec::new();
437
438

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

Daniel Brüning's avatar
Daniel Brüning committed
441
        //TODO: use user
442
        for (user, userdata) in groupdata {
443
444
            let mut userresults: Vec<String> = Vec::new();

445
446
447
            userresults.push(String::new());
            let mut summe = 0;

448
            for grade in userdata {
449
450
451
452
                if let Some(g) = grade.grade {
                    userresults.push(format!("{}", g));
                    summe += g;
                } else {
Robert Czechowski's avatar
Robert Czechowski committed
453
                    userresults.push("–".to_string());
454
                }
455
456
            }

457
458
            userresults[0] = format!("{}", summe);

459
460
            groupresults.push((user.firstname.unwrap_or_else(|| "–".to_string()),
                               user.lastname.unwrap_or_else(|| "–".to_string()),
461
                               user.id,
462
                               grade_to_string(user.grade),
463
                               userresults))
464
        }
465

Robert Czechowski's avatar
Robert Czechowski committed
466
        results.push((group.name.to_string(), group.id.unwrap_or(0), groupresults));
467
468
469
470
    }

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
471
472

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

475
    data.insert("contest".to_string(), to_json(&ci));
476
    data.insert("contestname".to_string(), to_json(&c.name));
477

478
479
480
    Ok(("contestresults".to_owned(), data))
}

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

487
    // Check logged in or open contest
488
    if contest.duration != 0 && !session.is_logged_in() {
489
        return Err(MedalError::AccessDenied);
490
    }
491

492
    // Check CSRF token
493
494
495
    if session.is_logged_in() && session.csrf_token != csrf_token {
        return Err(MedalError::CsrfCheckFailed);
    }
496

497
498
499
500
501
502
503
    // Check other constraints
    let constraints = check_contest_constraints(&session, &contest);

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

504
    // Start contest
505
    match conn.new_participation(&session_token, contest_id) {
Robert Czechowski's avatar
Robert Czechowski committed
506
        Ok(_) => Ok(()),
507
        _ => Err(MedalError::AccessDenied), // Contest already started TODO: Maybe redirect to page with hint
Robert Czechowski's avatar
Robert Czechowski committed
508
    }
Robert Czechowski's avatar
Robert Czechowski committed
509
510
}

511
512
pub fn login<T: MedalConnection>(conn: &T, login_data: (String, String), login_info: LoginInfo)
                                 -> Result<String, MedalValue> {
Robert Czechowski's avatar
Robert Czechowski committed
513
514
    let (username, password) = login_data;

515
    match conn.login(None, &username, &password) {
Robert Czechowski's avatar
Robert Czechowski committed
516
        Ok(session_token) => Ok(session_token),
Robert Czechowski's avatar
Robert Czechowski committed
517
518
        Err(()) => {
            let mut data = json_val::Map::new();
519
            data.insert("reason".to_string(), to_json(&"Login fehlgeschlagen. Bitte erneut versuchen.".to_string()));
Robert Czechowski's avatar
Robert Czechowski committed
520
            data.insert("username".to_string(), to_json(&username));
521
522
            data.insert("parent".to_string(), to_json(&"base"));

523
            fill_oauth_data(login_info, &mut data);
524

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

Robert Czechowski's avatar
Robert Czechowski committed
530
pub fn login_with_code<T: MedalConnection>(
531
    conn: &T, code: &str, login_info: LoginInfo)
Robert Czechowski's avatar
Robert Czechowski committed
532
    -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
533
    match conn.login_with_code(None, &code) {
Robert Czechowski's avatar
Robert Czechowski committed
534
535
536
537
538
539
540
        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));
541
542
                data.insert("parent".to_string(), to_json(&"base"));

543
                fill_oauth_data(login_info, &mut data);
544

Robert Czechowski's avatar
Robert Czechowski committed
545
                Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
546
            }
Robert Czechowski's avatar
Robert Czechowski committed
547
        },
Robert Czechowski's avatar
Robert Czechowski committed
548
549
550
    }
}

551
pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
552
    session_token.map(|token| conn.logout(&token));
Robert Czechowski's avatar
Robert Czechowski committed
553
554
}

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

559
    match match subtask {
Robert Czechowski's avatar
Robert Czechowski committed
560
561
562
              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
563
        Some(submission) => Ok(submission.value),
Robert Czechowski's avatar
Robert Czechowski committed
564
        None => Ok("{}".to_string()),
Robert Czechowski's avatar
Robert Czechowski committed
565
566
567
    }
}

568
569
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
570
571
                                           -> MedalResult<String>
{
572
    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
573
574

    if session.csrf_token != csrf_token {
575
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
576
577
    }

578
579
580
581
582
583
584
585
586
587
588
589
    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;
590
            if c.duration > 0 && left_secs < -10 {
591
592
593
                return Err(MedalError::AccessDenied);
                // Contest over
                // TODO: Nicer message!
594
595
596
597
            }
        }
    }

Robert Czechowski's avatar
Robert Czechowski committed
598
599
600
    let submission = Submission { id: None,
                                  session_user: session.id,
                                  task: task_id,
Robert Czechowski's avatar
Robert Czechowski committed
601
                                  grade,
Robert Czechowski's avatar
Robert Czechowski committed
602
603
604
605
606
607
                                  validated: false,
                                  nonvalidated_grade: grade,
                                  needs_validation: true,
                                  subtask_identifier: subtask,
                                  value: data,
                                  date: time::get_time() };
608

Robert Czechowski's avatar
Robert Czechowski committed
609
610
611
612
613
    conn.submit_submission(submission);

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

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

617
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
618
619
    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?
620

621
622
623
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
624

625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
    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);
        }
    }
640

641
    match conn.get_participation(&session_token, c.id.expect("Value from database")) {
642
        None => Err(MedalError::AccessDenied),
643
644
645
646
647
648
        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
649

650
            let mut data = json_val::Map::new();
651
            data.insert("participation_start_date".to_string(), to_json(&format!("{}", passed_secs)));
652
653
654
            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
655

656
            let left_secs = i64::from(c.duration) * 60 - passed_secs;
657
            if c.duration > 0 && left_secs < 0 {
658
                Err(MedalError::AccessDenied)
659
660
661
662
663
            // 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
664
665
                data.insert("time_left".to_string(), to_json(&format!("{}:{:02}", hour, min)));
                data.insert("time_left_sec".to_string(), to_json(&format!(":{:02}", sec)));
666
667

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

669
                data.insert("contestname".to_string(), to_json(&c.name));
670
671
                data.insert("name".to_string(), to_json(&tg.name));
                data.insert("taskid".to_string(), to_json(&task_id));
672
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
673
674
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
675
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
676

Robert Czechowski's avatar
Robert Czechowski committed
677
678
679
680
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

681
682
683
684
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
685
}
Robert Czechowski's avatar
Robert Czechowski committed
686

Robert Czechowski's avatar
Robert Czechowski committed
687
688
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
689
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
690
691
692
693
694
    pub name: String,
    pub tag: String,
    pub code: String,
}

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

Robert Czechowski's avatar
Robert Czechowski committed
698
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
699
700

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

Robert Czechowski's avatar
Robert Czechowski committed
703
704
705
706
707
708
709
710
    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
711
    data.insert("group".to_string(), to_json(&v));
712
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
713

714
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
715
716
}

Robert Czechowski's avatar
Robert Czechowski committed
717
718
#[derive(Serialize, Deserialize)]
pub struct MemberInfo {
719
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
720
721
    pub firstname: String,
    pub lastname: String,
722
    pub grade: String,
Robert Czechowski's avatar
Robert Czechowski committed
723
724
725
    pub logincode: String,
}

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

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

Robert Czechowski's avatar
Robert Czechowski committed
733
    if group.admin != session.id {
734
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
735
736
    }

Robert Czechowski's avatar
Robert Czechowski committed
737
738
739
740
741
742
743
744
745
746
747
    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()),
748
                                   grade: grade_to_string(m.grade),
Robert Czechowski's avatar
Robert Czechowski committed
749
750
                                   logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()) })
             .collect();
Robert Czechowski's avatar
Robert Czechowski committed
751
752
753

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
754
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
755
756
757

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

759
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
760
761
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
762

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

    if session.csrf_token != csrf_token {
768
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
769
770
    }

Robert Czechowski's avatar
Robert Czechowski committed
771
    let groupcode = helpers::make_group_code();
772
    // TODO: check for collisions
773

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

Robert Czechowski's avatar
Robert Czechowski committed
776
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
777
778
779

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

781
pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
782
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
783
784
785
786
787
788
789

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

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

790
// TODO: Should creating the users and groups happen in a batch operation to speed things up?
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
791
792
pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
                                         -> MedalResult<()> {
793
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
794
795
796
797
798

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

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

801
802
803
804
805
    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
806
807
808
809
810
811
812
    let mut group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: "".to_string(),
                            admin: session.id,
                            members: Vec::new() };

813
814
    for line in v {
        if name != line[0] {
815
            if name != "" {
816
                conn.create_group_with_users(group);
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
817
            }
818
819
820
821
            name = line[0].clone();
            group_code = helpers::make_group_code();
            // TODO: check for collisions

Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
822
823
824
825
826
827
            group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: name.clone(),
                            admin: session.id,
                            members: Vec::new() };
828
829
830
831
832
833
834
835
836
837
838
        }

        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);

839
840
841
    Ok(())
}

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

848
    let data = json_val::Map::new();
849
850
851

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

853
pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
Robert Czechowski's avatar
Robert Czechowski committed
854
855
856
                                        query_string: Option<String>)
                                        -> MedalValueResult
{
857
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
858
859

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

862
863
    match user_id {
        None => {
864
865
866
867
868
            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));
869
            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
870
871
872
873
874
            if let Some(sex) = session.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
875

876
            data.insert("profile_logincode".to_string(), to_json(&session.logincode));
877
            if session.password.is_some() {
878
                data.insert("profile_username".to_string(), to_json(&session.username));
879
880
            }
            if session.managed_by.is_none() {
881
                data.insert("profile_not_in_group".into(), to_json(&true));
882
            }
883
884
885
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
886
887
            data.insert("ownprofile".into(), to_json(&true));

888
            if let Some(query) = query_string {
889
                if query.starts_with("status=") {
890
                    let status: &str = &query[7..];
891
                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch", "firstlogin"].contains(&status) {
892
893
                        data.insert((status).to_string(), to_json(&true));
                    }
894
895
                }
            }
896

897
898
            let now = time::get_time();

899
            // TODO: Needs to be filtered
900
901
902
903
904
905
906
907
908
909
910
911
            let participations: Vec<(i32, String, bool, bool)> =
                conn.get_all_participations_complete(session.id)
                    .into_iter()
                    .rev()
                    .map(|(participation, contest)| {
                        let passed_secs = now.sec - participation.start.sec;
                        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
                        let is_time_left = contest.duration == 0 || left_secs >= 0;
                        let has_timelimit = contest.duration != 0;
                        (contest.id.unwrap(), contest.name, has_timelimit, is_time_left)
                    })
                    .collect();
912
            data.insert("participations".into(), to_json(&participations));
Robert Czechowski's avatar
Robert Czechowski committed
913
        }
914
915
916
917
918
919
920
921
        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);
            }

922
923
924
925
926
            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));
927
            data.insert(format!("sel{}", user.grade), to_json(&"selected"));
928
929
930
931
932
            if let Some(sex) = user.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
933

934
            data.insert("profile_logincode".to_string(), to_json(&user.logincode));
935
            if user.username.is_some() {
936
                data.insert("profile_username".to_string(), to_json(&user.username));
937
938
            }
            if user.managed_by.is_none() {
939
                data.insert("profile_not_in_group".into(), to_json(&true));
940
            }
941
942
943
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
944
            data.insert("ownprofile".into(), to_json(&false));
945

946
947
948
949
950
951
952
953
            if let Some(query) = query_string {
                if query.starts_with("status=") {
                    let status: &str = &query[7..];
                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
                        data.insert((status).to_string(), to_json(&true));
                    }
                }
            }
954
955
        }
    }
956

957
    Ok(("profile".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
958
959
}

960
#[derive(Debug, PartialEq)]
961
pub enum ProfileStatus {
Daniel Brüning's avatar
Daniel Brüning committed
962
963
964
965
    NothingChanged,
    DataChanged,
    PasswordChanged,
    PasswordMissmatch,
966
967
}
impl std::convert::Into<String> for ProfileStatus {
Robert Czechowski's avatar
Robert Czechowski committed
968
    fn into(self) -> String { format!("{:?}", self) }
969
970
}

971
pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str,
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
                                        (firstname,
                                         lastname,