core.rs 59.4 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
19
#[cfg(feature = "signup")]
use db_conn::SignupResult;
20
use db_objects::OptionSession;
21
use db_objects::SessionUser;
22
use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
23
24
25
use helpers;
use oauth_provider::OauthProvider;
use webfw_iron::{json_val, to_json};
26

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

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

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

49
50
#[derive(Clone)]
pub enum MedalError {
51
52
    NotLoggedIn,
    AccessDenied,
53
54
55
    CsrfCheckFailed,
    SessionTimeout,
    DatabaseError,
56
    PasswordHashingError,
57
    UnmatchedPasswords,
58
    NotFound,
59
    OauthError(String),
60
}
61

62
63
64
65
66
67
pub struct LoginInfo {
    pub password_login: bool,
    pub self_url: Option<String>,
    pub oauth_providers: Option<Vec<OauthProvider>>,
}

68
69
70
71
type MedalValue = (String, json_val::Map<String, json_val::Value>);
type MedalResult<T> = Result<T, MedalError>;
type MedalValueResult = MedalResult<MedalValue>;

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

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

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

99
    data.insert("self_url".to_string(), to_json(&login_info.self_url));
100
    data.insert("oauth_links".to_string(), to_json(&oauth_links));
101
102

    data.insert("password_login".to_string(), to_json(&login_info.password_login));
103
104
}

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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(),
    }
}

120
121
pub fn index<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo)
                                 -> (String, json_val::Map<String, json_val::Value>) {
122
123
124
125
    let mut data = json_val::Map::new();

    if let Some(token) = session_token {
        if let Some(session) = conn.get_session(&token) {
126
            fill_user_data(&session, &mut data);
127
128
129
        }
    }

130
    fill_oauth_data(login_info, &mut data);
131

132
    data.insert("parent".to_string(), to_json(&"base"));
133
134
135
    ("index".to_owned(), data)
}

136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
pub fn show_login<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo)
                                      -> (String, json_val::Map<String, json_val::Value>) {
    let mut data = json_val::Map::new();

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

    fill_oauth_data(login_info, &mut data);

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

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

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

196
197
198
199
#[derive(PartialEq, Eq)]
pub enum ContestVisibility {
    All,
    Open,
200
    Current,
201
    LoginRequired,
202
203
}

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

210
    let session = conn.get_session_or_new(&session_token);
211
    fill_user_data(&session, &mut data);
212

213
214
215
216
    if session.is_logged_in() {
        data.insert("can_start".to_string(), to_json(&true));
    }

217
    fill_oauth_data(login_info, &mut data);
218

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

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

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

258
259
260
261
262
        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
263
        let si = SubTaskInfo { id: st.id.unwrap(), linktext, active, greyout };
264
265
266
267
268
269

        subtaskinfos.push(si);
    }
    subtaskinfos
}

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

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

308
    let contest = conn.get_contest_by_id_complete(contest_id);
309
    let grades = conn.get_contest_user_grades(&session_token, contest_id);
310

311
    let mut opt_part = conn.get_participation(&session_token, contest_id);
312

313
314
315
    let ci = ContestInfo { id: contest.id.unwrap(),
                           name: contest.name.clone(),
                           duration: contest.duration,
316
                           public: contest.public };
Robert Czechowski's avatar
Robert Czechowski committed
317
318

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

325
    let constraints = check_contest_constraints(&session, &contest);
326

327
    let can_start = session.is_logged_in() && constraints.contest_running && constraints.grade_matching;
328
    let has_duration = contest.duration > 0;
329

330
    data.insert("constraints".to_string(), to_json(&constraints));
331
    data.insert("has_duration".to_string(), to_json(&has_duration));
332
    data.insert("can_start".to_string(), to_json(&can_start));
333

334
335
336
337
    let has_tasks = contest.taskgroups.len() > 0;
    data.insert("has_tasks".to_string(), to_json(&has_tasks));
    data.insert("no_tasks".to_string(), to_json(&!has_tasks));

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

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

378
379
380
    if let Some(participation) = opt_part {
        let mut totalgrade = 0;
        let mut max_totalgrade = 0;
381

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

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

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

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

422
423
424
425
426
427
    // 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));
428
429
    }

430
    Ok(("contest".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
431
432
}

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

438
439
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

440
    let mut results: Vec<(String, i32, Vec<(String, String, i32, String, Vec<String>)>)> = Vec::new();
441
442

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

Daniel Brüning's avatar
Daniel Brüning committed
445
        //TODO: use user
446
        for (user, userdata) in groupdata {
447
448
            let mut userresults: Vec<String> = Vec::new();

449
450
451
            userresults.push(String::new());
            let mut summe = 0;

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

461
462
            userresults[0] = format!("{}", summe);

463
464
            groupresults.push((user.firstname.unwrap_or_else(|| "–".to_string()),
                               user.lastname.unwrap_or_else(|| "–".to_string()),
465
                               user.id,
466
                               grade_to_string(user.grade),
467
                               userresults))
468
        }
469

Robert Czechowski's avatar
Robert Czechowski committed
470
        results.push((group.name.to_string(), group.id.unwrap_or(0), groupresults));
471
472
473
474
    }

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
475
476

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

479
    data.insert("contest".to_string(), to_json(&ci));
480
    data.insert("contestname".to_string(), to_json(&c.name));
481

482
483
484
    Ok(("contestresults".to_owned(), data))
}

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

491
    // Check logged in or open contest
492
    if contest.duration != 0 && !session.is_logged_in() {
493
        return Err(MedalError::AccessDenied);
494
    }
495

496
    // Check CSRF token
497
498
499
    if session.is_logged_in() && session.csrf_token != csrf_token {
        return Err(MedalError::CsrfCheckFailed);
    }
500

501
502
503
504
505
506
507
    // Check other constraints
    let constraints = check_contest_constraints(&session, &contest);

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

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

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

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

527
            fill_oauth_data(login_info, &mut data);
528

529
            Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
530
531
532
533
        }
    }
}

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

547
                fill_oauth_data(login_info, &mut data);
548

Robert Czechowski's avatar
Robert Czechowski committed
549
                Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
550
            }
Robert Czechowski's avatar
Robert Czechowski committed
551
        },
Robert Czechowski's avatar
Robert Czechowski committed
552
553
554
    }
}

555
pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
556
    session_token.map(|token| conn.logout(&token));
Robert Czechowski's avatar
Robert Czechowski committed
557
558
}

559
#[cfg(feature = "signup")]
560
561
562
563
564
565
566
567
568
569
570
571
572
573
pub fn signup<T: MedalConnection>(conn: &T, session_token:Option<String>, signup_data: (String, String, String)) -> MedalResult<SignupResult> {
    let (username, email, password) = signup_data;

    if username == "" || email == "" || password == "" {
        return Ok(SignupResult::EmptyFields)
    }

    let salt = helpers::make_salt();
    let hash = helpers::hash_password(&password, &salt)?;

    let result = conn.signup(&session_token.unwrap(), &username, &email, hash, &salt);
    Ok(result)
}

574
#[cfg(feature = "signup")]
575
576
577
578
579
580
581
582
583
584
585
586
587
pub fn signupdata (query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
    let mut data = json_val::Map::new();
    if let Some(query) = query_string {
        if query.starts_with("status=") {
            let status: &str = &query[7..];
            if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) {
                data.insert((status).to_string(), to_json(&true));
            }
        }
    }
    data
}

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

592
    match match subtask {
Robert Czechowski's avatar
Robert Czechowski committed
593
594
595
              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
596
        Some(submission) => Ok(submission.value),
Robert Czechowski's avatar
Robert Czechowski committed
597
        None => Ok("{}".to_string()),
Robert Czechowski's avatar
Robert Czechowski committed
598
599
600
    }
}

601
pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, csrf_token: &str,
602
                                           data: String, grade_percentage: i32, subtask: Option<String>)
Robert Czechowski's avatar
Robert Czechowski committed
603
604
                                           -> MedalResult<String>
{
605
    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
606
607

    if session.csrf_token != csrf_token {
608
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
609
610
    }

611
    let (t, _, c) = conn.get_task_by_id_complete(task_id);
612
613
614
615
616
617
618
619
620
621
622

    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;
623
            if c.duration > 0 && left_secs < -10 {
624
625
626
                return Err(MedalError::AccessDenied);
                // Contest over
                // TODO: Nicer message!
627
628
629
630
            }
        }
    }

631
632
    /* Here, two variants of the grade are calculated. Which one is correct depends on how the percentage value is
     * calculated in the task. Currently, grade_rounded is the correct one, but if that ever changes, the other code
633
634
635
636
637
     * can just be used.
     *
     * Switch to grade_truncated, when a user scores 98/99 but only gets 97/99 awarded.
     * Switch to grade_rounded, when a user scores 5/7 but only gets 4/7 awarded.
     */
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663

    /* Code for percentages calculated with integer rounding.
     *
     * This is a poor man's rounding that only works for division by 100.
     *
     *   floor((floor((x*10)/100)+5)/10) = round(x/100)
     */
    let grade_rounded = ((grade_percentage * t.stars * 10) / 100 + 5) / 10;

    /* Code for percentages calculated with integer truncation.
     *
     * Why add one to grade_percentage and divide by 101?
     *
     * For all m in 1..100 and all n in 0..n, this holds:
     *
     *   floor( ((floor(n / m * 100)+1) * m ) / 101 ) = n
     *
     * Thus, when percentages are calculated as
     *
     *   p = floor(n / m * 100)
     *
     * we can recover n by using
     *
     *   n = floor( ((p+1) * m) / 101 )
     */
    // let grade_truncated = ((grade_percentage+1) * t.stars) / 101;
664

Robert Czechowski's avatar
Robert Czechowski committed
665
666
667
    let submission = Submission { id: None,
                                  session_user: session.id,
                                  task: task_id,
668
                                  grade: grade_rounded,
Robert Czechowski's avatar
Robert Czechowski committed
669
                                  validated: false,
670
                                  nonvalidated_grade: grade_rounded,
Robert Czechowski's avatar
Robert Czechowski committed
671
672
673
674
                                  needs_validation: true,
                                  subtask_identifier: subtask,
                                  value: data,
                                  date: time::get_time() };
675

Robert Czechowski's avatar
Robert Czechowski committed
676
677
678
679
680
    conn.submit_submission(submission);

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

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

684
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
685
686
    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?
687

688
689
690
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
691

692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
    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);
        }
    }
707

708
    match conn.get_participation(&session_token, c.id.expect("Value from database")) {
709
        None => Err(MedalError::AccessDenied),
710
711
712
713
714
715
        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
716

717
            let mut data = json_val::Map::new();
718
            data.insert("participation_start_date".to_string(), to_json(&format!("{}", passed_secs)));
719
720
721
            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
722

723
            let left_secs = i64::from(c.duration) * 60 - passed_secs;
724
            if c.duration > 0 && left_secs < 0 {
725
                Err(MedalError::AccessDenied)
726
727
728
729
730
            // 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
731
732
                data.insert("time_left".to_string(), to_json(&format!("{}:{:02}", hour, min)));
                data.insert("time_left_sec".to_string(), to_json(&format!(":{:02}", sec)));
733
734

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

736
                data.insert("contestname".to_string(), to_json(&c.name));
737
738
                data.insert("name".to_string(), to_json(&tg.name));
                data.insert("taskid".to_string(), to_json(&task_id));
739
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
740
741
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
742
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
743

Robert Czechowski's avatar
Robert Czechowski committed
744
745
746
747
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

748
749
750
751
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
752
}
Robert Czechowski's avatar
Robert Czechowski committed
753

Robert Czechowski's avatar
Robert Czechowski committed
754
755
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
756
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
757
758
759
760
761
    pub name: String,
    pub tag: String,
    pub code: String,
}

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

Robert Czechowski's avatar
Robert Czechowski committed
765
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
766
767

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

Robert Czechowski's avatar
Robert Czechowski committed
770
771
772
773
774
775
776
777
    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
778
    data.insert("group".to_string(), to_json(&v));
779
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
780

781
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
782
783
}

Robert Czechowski's avatar
Robert Czechowski committed
784
785
#[derive(Serialize, Deserialize)]
pub struct MemberInfo {
786
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
787
788
    pub firstname: String,
    pub lastname: String,
789
    pub grade: String,
Robert Czechowski's avatar
Robert Czechowski committed
790
791
792
    pub logincode: String,
}

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

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

Robert Czechowski's avatar
Robert Czechowski committed
800
    if group.admin != session.id {
801
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
802
803
    }

Robert Czechowski's avatar
Robert Czechowski committed
804
805
806
807
808
809
810
811
812
813
814
    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()),
815
                                   grade: grade_to_string(m.grade),
Robert Czechowski's avatar
Robert Czechowski committed
816
817
                                   logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()) })
             .collect();
Robert Czechowski's avatar
Robert Czechowski committed
818
819
820

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
821
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
822
823
824

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

826
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
827
828
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
829

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

    if session.csrf_token != csrf_token {
835
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
836
837
    }

Robert Czechowski's avatar
Robert Czechowski committed
838
    let groupcode = helpers::make_group_code();
839
    // TODO: check for collisions
840

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

Robert Czechowski's avatar
Robert Czechowski committed
843
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
844
845
846

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

848
pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
849
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
850
851
852
853
854
855
856

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

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

857
// TODO: Should creating the users and groups happen in a batch operation to speed things up?
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
858
859
pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
                                         -> MedalResult<()> {
860
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
861
862
863
864
865

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

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

868
869
870
871
872
    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
873
874
875
876
877
878
879
    let mut group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: "".to_string(),
                            admin: session.id,
                            members: Vec::new() };

880
881
    for line in v {
        if name != line[0] {
882
            if name != "" {
883
                conn.create_group_with_users(group);
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
884
            }
885
886
887
888
            name = line[0].clone();
            group_code = helpers::make_group_code();
            // TODO: check for collisions

Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
889
890
891
892
893
894
            group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: name.clone(),
                            admin: session.id,
                            members: Vec::new() };
895
896
897
898
899
900
901
902
903
904
905
        }

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

906
907
908
    Ok(())
}

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

915
    let data = json_val::Map::new();
916
917
918

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

920
pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
Robert Czechowski's avatar
Robert Czechowski committed
921
922
923
                                        query_string: Option<String>)
                                        -> MedalValueResult
{
924
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
925
926

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

929
930
    match user_id {
        None => {
931
932
933
934
935
            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));
936
            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
937
938
939
940
941
            if let Some(sex) = session.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
942

943
            data.insert("profile_logincode".to_string(), to_json(&session.logincode));
944
            if session.password.is_some() {
945
                data.insert("profile_username".to_string(), to_json(&session.username));
946
947
            }
            if session.managed_by.is_none() {
948
                data.insert("profile_not_in_group".into(), to_json(&true));
949
            }
950
951
952
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
953
954
            data.insert("ownprofile".into(), to_json(&true));

955
            if let Some(query) = query_string {
956
                if query.starts_with("status=") {
957
                    let status: &str = &query[7..];
958
                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch", "firstlogin", "SignedUp"].contains(&status) {
959
960
                        data.insert((status).to_string(), to_json(&true));
                    }
961
962
                }
            }
963

964
965
            let now = time::get_time();

966
            // TODO: Needs to be filtered
967
968
969
970
971
972
973
974
975
976
977
978
            let participations: Vec<