core.rs 50.7 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
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
92
pub fn index<T: MedalConnection>(conn: &T, session_token: Option<String>,
93
                                 (self_url, oauth_providers): (Option<String>, Option<Vec<OauthProvider>>))
Robert Czechowski's avatar
Robert Czechowski committed
94
95
                                 -> (String, json_val::Map<String, json_val::Value>)
{
96
97
98
99
100
101
    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) {
102
            fill_user_data(&session, &mut data);
103
104
105
        }
    }

106
    let mut oauth_links: Vec<(String, String, String)> = Vec::new();
107
108
109
110
111
112
113
114
    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()));
        }
    }

115
    data.insert("self_url".to_string(), to_json(&self_url));
116
    data.insert("oauth_links".to_string(), to_json(&oauth_links));
117

118
    data.insert("parent".to_string(), to_json(&"base"));
119
120
121
    ("index".to_owned(), data)
}

122
123
pub fn status<T: MedalConnection>(conn: &T) -> String { conn.get_debug_information() }

124
125
126
127
128
129
130
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));
131
            data.insert("session_id".to_string(), to_json(&session.id));
132
133
134
135
136
137
138
139
140
141
142
143
144
            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));
145
146
                    data.insert("oauth_provider".to_string(), to_json(&session.oauth_provider));
                    data.insert("oauth_id".to_string(), to_json(&session.oauth_foreign_id));
147
148
                    data.insert("logincode".to_string(), to_json(&session.logincode));
                    data.insert("managed_by".to_string(), to_json(&session.managed_by));
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
                }
            }
        }
        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);
    }
}

166
167
168
169
#[derive(PartialEq, Eq)]
pub enum ContestVisibility {
    All,
    Open,
170
    Current,
171
    LoginRequired,
172
173
}

174
175
176
177
178
pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str,
                                         (self_url, oauth_providers): (Option<String>, Option<Vec<OauthProvider>>),
                                         visibility: ContestVisibility)
                                         -> MedalValue
{
Robert Czechowski's avatar
Robert Czechowski committed
179
180
    let mut data = json_val::Map::new();

181
    let session = conn.get_session_or_new(&session_token);
182
    fill_user_data(&session, &mut data);
183

184
185
186
187
    if session.is_logged_in() {
        data.insert("can_start".to_string(), to_json(&true));
    }

188
189
190
191
192
193
194
195
196
197
198
199
    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));

200
201
202
203
204
205
206
207
208
    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)
209
210
211
212
213
            .filter(|c| {
                !c.requires_login.unwrap_or(false)
                || visibility == ContestVisibility::LoginRequired
                || visibility == ContestVisibility::All
            })
214
215
216
            .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
217
    data.insert("contest".to_string(), to_json(&v));
218
219
    data.insert("contestlist_header".to_string(),
                to_json(&match visibility {
220
                            ContestVisibility::Open => "Trainingsaufgaben",
221
                            ContestVisibility::Current => "Aktuelle Wettbewerbe",
222
                            ContestVisibility::LoginRequired => "Herausforderungen",
223
224
                            ContestVisibility::All => "Alle Wettbewerbe",
                        }));
Robert Czechowski's avatar
Robert Czechowski committed
225
226
227
228

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

229
fn generate_subtaskstars(tg: &Taskgroup, grade: &Grade, ast: Option<i32>) -> Vec<SubTaskInfo> {
230
231
    let mut subtaskinfos = Vec::new();
    let mut not_print_yet = true;
232
    for st in &tg.tasks {
233
234
235
236
237
238
        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;
        }

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

        subtaskinfos.push(si);
    }
    subtaskinfos
}

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
279
280
281
282
#[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 }
}

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

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

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

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

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

300
    let constraints = check_contest_constraints(&session, &contest);
301

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

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

309
310
311
312
    // 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) {
313
314
315
316
317
318
    if opt_part.is_none()
       && contest.duration == 0
       && constraints.contest_running
       && constraints.grade_matching
       && contest.requires_login != Some(true)
    {
319
320
321
322
        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() });
    }

323
    let now = time::get_time();
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
    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));
347
348
    }

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

353
        let mut tasks = Vec::new();
354
        for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
355
356
357
358
359
360
361
            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);
        }
362
        let relative_points = (totalgrade * 100) / max_totalgrade;
363
364

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

366
367
368
369
370
        let now = time::get_time();
        let passed_secs = now.sec - participation.start.sec;
        if passed_secs < 0 {
            // behandle inkonsistente Serverzeit
        }
371
372
        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
        let is_time_left = contest.duration == 0 || left_secs >= 0;
373
374
375
376
        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);
377

378
        data.insert("is_started".to_string(), to_json(&true));
379
        data.insert("participation_start_date".to_string(), to_json(&passed_secs));
380
381
        data.insert("total_points".to_string(), to_json(&totalgrade));
        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
382
383
384
        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));
385
386
387
388
389
390
        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
391
    }
392

393
394
395
396
397
398
    // 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));
399
400
    }

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

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

409
410
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

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

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

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

420
421
422
            userresults.push(String::new());
            let mut summe = 0;

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

432
433
            userresults[0] = format!("{}", summe);

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

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

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
446
447

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

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

453
454
455
    Ok(("contestresults".to_owned(), data))
}

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

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

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

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

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

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

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

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

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

512
            Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
513
514
515
516
        }
    }
}

Robert Czechowski's avatar
Robert Czechowski committed
517
pub fn login_with_code<T: MedalConnection>(
518
    conn: &T, code: &str, (self_url, oauth_providers): (Option<String>, Option<Vec<OauthProvider>>))
Robert Czechowski's avatar
Robert Czechowski committed
519
    -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
520
    match conn.login_with_code(None, &code) {
Robert Czechowski's avatar
Robert Czechowski committed
521
522
523
524
525
526
527
        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));
528
529
530
531
532
533
534
535
536
537
538
539
540
541
                data.insert("parent".to_string(), to_json(&"base"));

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

Robert Czechowski's avatar
Robert Czechowski committed
542
                Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
543
            }
Robert Czechowski's avatar
Robert Czechowski committed
544
        },
Robert Czechowski's avatar
Robert Czechowski committed
545
546
547
    }
}

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

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

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

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

    if session.csrf_token != csrf_token {
572
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
573
574
    }

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

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

Robert Czechowski's avatar
Robert Czechowski committed
606
607
608
609
610
    conn.submit_submission(submission);

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

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

614
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
615
616
    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?
617

618
619
620
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
621

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

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

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

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

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

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

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

678
679
680
681
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
682
}
Robert Czechowski's avatar
Robert Czechowski committed
683

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

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

Robert Czechowski's avatar
Robert Czechowski committed
695
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
696
697

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

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

711
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
712
713
}

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

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

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

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

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

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

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

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

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

    if session.csrf_token != csrf_token {
765
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
766
767
    }

768
    let group_code = helpers::make_group_code();
769
    // TODO: check for collisions
770

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

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

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

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

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

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

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

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

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

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

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

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

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

837
838
839
    Ok(())
}

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

846
    let data = json_val::Map::new();
847
848
849

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

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

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

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

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

886
            if let Some(query) = query_string {
887
                if query.starts_with("status=") {
888
                    let status: &str = &query[7..];
889
                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch", "firstlogin"].contains(&status) {
890
891
                        data.insert((status).to_string(), to_json(&true));
                    }
892
893
                }
            }
Robert Czechowski's avatar
Robert Czechowski committed
894
        }
895
896
897
898
899
900
901
902
        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);
            }

903
904
905
906
907
            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));
908
            data.insert(format!("sel{}", user.grade), to_json(&"selected"));
909
910
911
912
913
            if let Some(sex) = user.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
914

915
            data.insert("profile_logincode".to_string(), to_json(&user.logincode));
916
            if user.username.is_some() {
917
                data.insert("profile_username".to_string(), to_json(&user.username));
918
919
            }
            if user.managed_by.is_none() {
920
                data.insert("profile_not_in_group".into(), to_json(&true));
921
            }
922
923
924
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
925
            data.insert("ownprofile".into(), to_json(&false));
926

927
928
929
930
931
932
933
934
            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));
                    }
                }
            }
935
936
        }
    }
937

938
    Ok(("profile".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
939
940
}

941
#[derive(Debug, PartialEq)]
942
pub enum ProfileStatus {
Daniel Brüning's avatar
Daniel Brüning committed
943
944
945
946
    NothingChanged,
    DataChanged,
    PasswordChanged,
    PasswordMissmatch,
947
948
}
impl std::convert::Into<String> for ProfileStatus {
Robert Czechowski's avatar
Robert Czechowski committed
949
    fn into(self) -> String { format!("{:?}", self) }
950
951
}

952
pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str,
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
                                        (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
970
971
                                        -> MedalResult<ProfileStatus>
{
972
    let mut session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
973
974

    if session.csrf_token != csrf_token {
975
        return Err(MedalError::AccessDenied); // CsrfError
Robert Czechowski's avatar
Robert Czechowski committed
976
977
    }

978
    let mut result = ProfileStatus::NothingChanged;
979

980
    let mut password_and_salt = None;
981

982
983
984
    if let (Some(password), Some(password_repeat)) = (password, password_repeat) {
        if password != "" || password_repeat != "" {
            if password == password_repeat {