core.rs 59.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*  medal                                                                                                            *\
 *  Copyright (C) 2020  Bundesweite Informatikwettbewerbe                                                            *
 *                                                                                                                   *
 *  This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero        *
 *  General Public License as published  by the Free Software Foundation, either version 3 of the License, or (at    *
 *  your option) any later version.                                                                                  *
 *                                                                                                                   *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the       *
 *  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public      *
 *  License for more details.                                                                                        *
 *                                                                                                                   *
 *  You should have received a copy of the GNU Affero General Public License along with this program.  If not, see   *
\*  <http://www.gnu.org/licenses/>.                                                                                  */

15
16
use time;

Robert Czechowski's avatar
Robert Czechowski committed
17
use db_conn::MedalConnection;
18
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")]
Robert Czechowski's avatar
Robert Czechowski committed
560
561
pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signup_data: (String, String, String))
                                  -> MedalResult<SignupResult> {
562
563
564
    let (username, email, password) = signup_data;

    if username == "" || email == "" || password == "" {
Robert Czechowski's avatar
Robert Czechowski committed
565
        return Ok(SignupResult::EmptyFields);
566
567
568
569
570
571
572
573
574
    }

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

575
#[cfg(feature = "signup")]
Robert Czechowski's avatar
Robert Czechowski committed
576
pub fn signupdata(query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
577
578
579
580
581
582
583
584
585
586
587
588
    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
}

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

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

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

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

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

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

632
633
    /* 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
634
635
636
637
638
     * 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.
     */
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
664

    /* 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;
665

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

907
908
909
    Ok(())
}

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

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

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

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

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

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

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

956
            if let Some(query) = query_string {
957
                if query.starts_with("status=") {
958
                    let status: &str = &query[7..];
Robert Czechowski's avatar
Robert Czechowski committed
959
960
961
962
963
964
965
                    if ["NothingChanged",
                        "DataChanged",
                        "PasswordChanged",
                        "PasswordMissmatch",
                        "firstlogin",
                        "SignedUp"].contains(&status)
                    {
966
967
                        data.insert((status).to_string(), to_json(&true));
                    }
968
969
                }
            }
970