core.rs 59.2 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
}
60

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

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

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

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

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

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

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

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

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

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

129
    fill_oauth_data(login_info, &mut data);
130

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

135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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)
}

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

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

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

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

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

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

216
    fill_oauth_data(login_info, &mut data);
217

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

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

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

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

        subtaskinfos.push(si);
    }
    subtaskinfos
}

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

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

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

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

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

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

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

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

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

333
334
335
336
    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));

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

526
            fill_oauth_data(login_info, &mut data);
527

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

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

546
                fill_oauth_data(login_info, &mut data);
547

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

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

558
#[cfg(feature = "signup")]
559
560
561
562
563
564
565
566
567
568
569
570
571
572
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)
}

573
#[cfg(feature = "signup")]
574
575
576
577
578
579
580
581
582
583
584
585
586
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
}

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

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

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

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

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

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

630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
    /* 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
     * can just be used */

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

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

Robert Czechowski's avatar
Robert Czechowski committed
671
672
673
674
675
    conn.submit_submission(submission);

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

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

679
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
680
681
    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?
682

683
684
685
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
686

687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
    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);
        }
    }
702

703
    match conn.get_participation(&session_token, c.id.expect("Value from database")) {
704
        None => Err(MedalError::AccessDenied),
705
706
707
708
709
710
        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
711

712
            let mut data = json_val::Map::new();
713
            data.insert("participation_start_date".to_string(), to_json(&format!("{}", passed_secs)));
714
715
716
            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
717

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

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

731
                data.insert("contestname".to_string(), to_json(&c.name));
732
733
                data.insert("name".to_string(), to_json(&tg.name));
                data.insert("taskid".to_string(), to_json(&task_id));
734
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
735
736
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
737
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
738

Robert Czechowski's avatar
Robert Czechowski committed
739
740
741
742
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

743
744
745
746
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
747
}
Robert Czechowski's avatar
Robert Czechowski committed
748

Robert Czechowski's avatar
Robert Czechowski committed
749
750
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
751
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
752
753
754
755
756
    pub name: String,
    pub tag: String,
    pub code: String,
}

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

Robert Czechowski's avatar
Robert Czechowski committed
760
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
761
762

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

Robert Czechowski's avatar
Robert Czechowski committed
765
766
767
768
769
770
771
772
    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
773
    data.insert("group".to_string(), to_json(&v));
774
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
775

776
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
777
778
}

Robert Czechowski's avatar
Robert Czechowski committed
779
780
#[derive(Serialize, Deserialize)]
pub struct MemberInfo {
781
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
782
783
    pub firstname: String,
    pub lastname: String,
784
    pub grade: String,
Robert Czechowski's avatar
Robert Czechowski committed
785
786
787
    pub logincode: String,
}

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

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

Robert Czechowski's avatar
Robert Czechowski committed
795
    if group.admin != session.id {
796
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
797
798
    }

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

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
816
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
817
818
819

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

821
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
822
823
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
824

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

    if session.csrf_token != csrf_token {
830
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
831
832
    }

Robert Czechowski's avatar
Robert Czechowski committed
833
    let groupcode = helpers::make_group_code();
834
    // TODO: check for collisions
835

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

Robert Czechowski's avatar
Robert Czechowski committed
838
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
839
840
841

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

843
pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
844
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
845
846
847
848
849
850
851

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

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

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

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

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

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

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

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

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

901
902
903
    Ok(())
}

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

910
    let data = json_val::Map::new();
911
912
913

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

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

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

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

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

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

959
960
            let now = time::get_time();

961
            // TODO: Needs to be filtered
962
963
964
965
966
967
968
969
970
971
972
973
            let participations: Vec<(i32, String, bool, bool)> =
                conn.get_all_participations_complete(session.id)
                    .into_iter()
                    .rev()
                    .map(|(participation, contest)| {
                        let passed_secs = now.sec - participation.start.sec;
                        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
                        let is_time_left = contest.duration == 0 || left_secs >= 0;
                        let has_timelimit = contest.duration != 0;
                        (contest.id.unwrap(), contest.name, has_timelimit, is_time_left)
                    })
                    .collect();
974
            data.insert("participations".into(), to_json(&participations));
Robert Czechowski's avatar