core.rs 55.5 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
        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
240
        let si = SubTaskInfo { id: st.id.unwrap(), 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
                if let Some(g) = grade.grade {
                    userresults.push(format!("{}", g));
                    summe += g;
                } else {
Robert Czechowski's avatar
Robert Czechowski committed
430
                    userresults.push("–".to_string());
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

Robert Czechowski's avatar
Robert Czechowski committed
443
        results.push((group.name.to_string(), 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
    let submission = Submission { id: None,
                                  session_user: session.id,
                                  task: task_id,
Robert Czechowski's avatar
Robert Czechowski committed
580
                                  grade,
Robert Czechowski's avatar
Robert Czechowski committed
581
582
583
584
585
586
                                  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
    }

Robert Czechowski's avatar
Robert Czechowski committed
750
    let groupcode = helpers::make_group_code();
751
    // TODO: check for collisions
752

Robert Czechowski's avatar
Robert Czechowski committed
753
    let mut group =
Robert Czechowski's avatar
Robert Czechowski committed
754
        Group { id: None, name, groupcode, 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", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
                        data.insert((status).to_string(), to_json(&true));
                    }
                }
            }
917
918
        }
    }
919

920
    Ok(("profile".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
921
922
}

923
#[derive(Debug, PartialEq)]
924
pub enum ProfileStatus {
Daniel Brüning's avatar
Daniel Brüning committed
925
926
927
928
    NothingChanged,
    DataChanged,
    PasswordChanged,
    PasswordMissmatch,
929
930
}
impl std::convert::Into<String> for ProfileStatus {
Robert Czechowski's avatar
Robert Czechowski committed
931
    fn into(self) -> String { format!("{:?}", self) }
932
933
}

934
pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str,
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
                                        (firstname,
                                         lastname,
                                         street,
                                         zip,
                                         city,
                                         password,
                                         password_repeat,
                                         grade,
                                         sex): (String,
                                         String,
                                         Option<String>,
                                         Option<String>,
                                         Option<String>,
                                         Option<String>,
                                         Option<String>,
                                         i32,
                                         Option<i32>))
Robert Czechowski's avatar
Robert Czechowski committed
952
953
                                        -> MedalResult<ProfileStatus>
{
954
    let mut session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;