core.rs 64.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
use helpers;
24
use config::OauthProvider;
25
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
#[derive(Clone, Debug)]
50
pub enum MedalError {
51
52
    NotLoggedIn,
    AccessDenied,
53
54
55
    CsrfCheckFailed,
    SessionTimeout,
    DatabaseError,
56
    ConfigurationError,
57
    DatabaseConnectionError,
58
    PasswordHashingError,
59
    UnmatchedPasswords,
60
    NotFound,
61
    OauthError(String),
62
}
63

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

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

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

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

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

102
    data.insert("self_url".to_string(), to_json(&login_info.self_url));
103
    data.insert("oauth_links".to_string(), to_json(&oauth_links));
104
105

    data.insert("password_login".to_string(), to_json(&login_info.password_login));
106
107
}

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

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

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

133
    fill_oauth_data(login_info, &mut data);
134

135
    data.insert("parent".to_string(), to_json(&"base"));
136
137
138
    ("index".to_owned(), data)
}

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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)
}

155
156
157
158
159
160
161
pub fn status<T: MedalConnection>(conn: &T, config_secret: Option<String>, given_secret: Option<String>) -> MedalResult<String> {
    if config_secret == given_secret {
        Ok(conn.get_debug_information())
    } else {
        Err(MedalError::AccessDenied)
    }
}
162

163
164
165
166
167
168
169
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));
170
            data.insert("session_id".to_string(), to_json(&session.id));
171
172
173
174
175
176
177
178
179
180
181
182
183
            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));
184
185
                    data.insert("oauth_provider".to_string(), to_json(&session.oauth_provider));
                    data.insert("oauth_id".to_string(), to_json(&session.oauth_foreign_id));
186
187
                    data.insert("logincode".to_string(), to_json(&session.logincode));
                    data.insert("managed_by".to_string(), to_json(&session.managed_by));
188
189
190
191
192
193
194
195
196
197
198
199
200
                }
            }
        }
        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 {
201
        conn.get_session_or_new(&token).unwrap();
202
203
204
    }
}

205
206
207
208
#[derive(PartialEq, Eq)]
pub enum ContestVisibility {
    All,
    Open,
209
    Current,
210
    LoginRequired,
211
212
}

213
pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str, login_info: LoginInfo,
214
                                         visibility: ContestVisibility)
215
                                         -> MedalValueResult
216
{
Robert Czechowski's avatar
Robert Czechowski committed
217
218
    let mut data = json_val::Map::new();

219
    let session = conn.get_session_or_new(&session_token).map_err(|_| MedalError::DatabaseConnectionError)?;
220
    fill_user_data(&session, &mut data);
221

222
223
224
225
    if session.is_logged_in() {
        data.insert("can_start".to_string(), to_json(&true));
    }

226
    fill_oauth_data(login_info, &mut data);
227

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

254
    Ok(("contests".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
255
256
}

257
fn generate_subtaskstars(tg: &Taskgroup, grade: &Grade, ast: Option<i32>) -> Vec<SubTaskInfo> {
258
259
    let mut subtaskinfos = Vec::new();
    let mut not_print_yet = true;
260
    for st in &tg.tasks {
261
262
263
264
265
266
        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;
        }

267
268
269
270
271
        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
272
        let si = SubTaskInfo { id: st.id.unwrap(), linktext, active, greyout };
273
274
275
276
277
278

        subtaskinfos.push(si);
    }
    subtaskinfos
}

279
280
281
282
283
284
285
286
287
288
#[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,
}

289
fn check_contest_qualification<T: MedalConnection>(conn: &T, session: &SessionUser, contest: &Contest) -> Option<bool> {
290
    let required_contests = contest.requires_contest.as_ref()?.split(',');
291
292
293
294
295
296
297
298
299
300

    for req_contest in required_contests {
        if conn.has_participation_by_contest_file(session.id, &contest.location, req_contest) {
            return Some(true);
        }
    }

    Some(false)
}

301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
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,
320
321
                              grade_matching,
                            }
322
323
}

324
pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
325
                                        query_string: Option<String>, login_info: LoginInfo)
326
327
                                        -> MedalValueResult
{
328
    let session = conn.get_session_or_new(&session_token).unwrap();
329

330
    let contest = conn.get_contest_by_id_complete(contest_id);
331
    let grades = conn.get_contest_user_grades(&session_token, contest_id);
332

333
    let mut opt_part = conn.get_participation(session.id, contest_id);
334

335
336
337
    let ci = ContestInfo { id: contest.id.unwrap(),
                           name: contest.name.clone(),
                           duration: contest.duration,
338
                           public: contest.public };
Robert Czechowski's avatar
Robert Czechowski committed
339
340

    let mut data = json_val::Map::new();
341
342
    data.insert("parent".to_string(), to_json(&"base"));
    data.insert("empty".to_string(), to_json(&"empty"));
Robert Czechowski's avatar
Robert Czechowski committed
343
    data.insert("contest".to_string(), to_json(&ci));
344
    data.insert("title".to_string(), to_json(&ci.name));
345
    data.insert("message".to_string(), to_json(&contest.message));
346
    fill_oauth_data(login_info, &mut data);
Robert Czechowski's avatar
Robert Czechowski committed
347

348
    let constraints = check_contest_constraints(&session, &contest);
349
    let is_qualified = check_contest_qualification(conn, &session, &contest).unwrap_or(true);
350

351
    let can_start = session.is_logged_in() && constraints.contest_running && constraints.grade_matching && is_qualified;
352
    let has_duration = contest.duration > 0;
353

354
    data.insert("constraints".to_string(), to_json(&constraints));
355
    data.insert("is_qualified".to_string(), to_json(&is_qualified));
356
    data.insert("has_duration".to_string(), to_json(&has_duration));
357
    data.insert("can_start".to_string(), to_json(&can_start));
358

359
360
361
362
    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));

363
364
365
366
    // 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) {
367
368
369
370
371
372
    if opt_part.is_none()
       && contest.duration == 0
       && constraints.contest_running
       && constraints.grade_matching
       && contest.requires_login != Some(true)
    {
373
374
375
376
        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() });
    }

377
    let now = time::get_time();
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
    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));
401
402
    }

403
404
405
    if let Some(participation) = opt_part {
        let mut totalgrade = 0;
        let mut max_totalgrade = 0;
406

407
        let mut tasks = Vec::new();
408
        for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
409
410
411
412
413
414
415
            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);
        }
416
        let relative_points = (totalgrade * 100) / max_totalgrade;
417
418

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

420
421
422
423
424
        let now = time::get_time();
        let passed_secs = now.sec - participation.start.sec;
        if passed_secs < 0 {
            // behandle inkonsistente Serverzeit
        }
425
426
        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
        let is_time_left = contest.duration == 0 || left_secs >= 0;
427
428
429
430
        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);
431

432
        data.insert("is_started".to_string(), to_json(&true));
433
        data.insert("participation_start_date".to_string(), to_json(&passed_secs));
434
435
        data.insert("total_points".to_string(), to_json(&totalgrade));
        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
436
437
438
        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));
439
440
441
442
443
444
        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));
        }
445
        data.insert("no_login".to_string(), to_json(&true));
Robert Czechowski's avatar
Robert Czechowski committed
446
    }
447

448
449
450
451
452
453
    // 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));
454
455
    }

456
    Ok(("contest".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
457
458
}

459
pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
460
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
461
462
    let mut data = json_val::Map::new();
    fill_user_data(&session, &mut data);
463

464
465
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

466
    let mut results: Vec<(String, i32, Vec<(String, String, i32, String, Vec<String>)>)> = Vec::new();
467
468

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

Daniel Brüning's avatar
Daniel Brüning committed
471
        //TODO: use user
472
        for (user, userdata) in groupdata {
473
474
            let mut userresults: Vec<String> = Vec::new();

475
476
477
            userresults.push(String::new());
            let mut summe = 0;

478
            for grade in userdata {
479
480
481
482
                if let Some(g) = grade.grade {
                    userresults.push(format!("{}", g));
                    summe += g;
                } else {
Robert Czechowski's avatar
Robert Czechowski committed
483
                    userresults.push("–".to_string());
484
                }
485
486
            }

487
488
            userresults[0] = format!("{}", summe);

489
490
            groupresults.push((user.firstname.unwrap_or_else(|| "–".to_string()),
                               user.lastname.unwrap_or_else(|| "–".to_string()),
491
                               user.id,
492
                               grade_to_string(user.grade),
493
                               userresults))
494
        }
495

Robert Czechowski's avatar
Robert Czechowski committed
496
        results.push((group.name.to_string(), group.id.unwrap_or(0), groupresults));
497
498
499
500
    }

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
501
502

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

505
    data.insert("contest".to_string(), to_json(&ci));
506
    data.insert("contestname".to_string(), to_json(&c.name));
507

508
509
510
    Ok(("contestresults".to_owned(), data))
}

511
pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str)
Robert Czechowski's avatar
Robert Czechowski committed
512
                                         -> MedalResult<()> {
513
    // TODO: Is _or_new the right semantic? We need a CSRF token anyway …
514
    let session = conn.get_session_or_new(&session_token).unwrap();
515
    let contest = conn.get_contest_by_id(contest_id);
516

517
    // Check logged in or open contest
518
    if contest.duration != 0 && !session.is_logged_in() {
519
        return Err(MedalError::AccessDenied);
520
    }
521

522
    // Check CSRF token
523
524
525
    if session.is_logged_in() && session.csrf_token != csrf_token {
        return Err(MedalError::CsrfCheckFailed);
    }
526

527
528
529
530
531
532
533
    // Check other constraints
    let constraints = check_contest_constraints(&session, &contest);

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

534
535
536
537
538
539
    let is_qualified = check_contest_qualification(conn, &session, &contest);

    if is_qualified == Some(false) {
        return Err(MedalError::AccessDenied);
    }

540
    // Start contest
541
    match conn.new_participation(&session_token, contest_id) {
Robert Czechowski's avatar
Robert Czechowski committed
542
        Ok(_) => Ok(()),
543
        _ => Err(MedalError::AccessDenied), // Contest already started TODO: Maybe redirect to page with hint
Robert Czechowski's avatar
Robert Czechowski committed
544
    }
Robert Czechowski's avatar
Robert Czechowski committed
545
546
}

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

551
    match conn.login(None, &username, &password) {
Robert Czechowski's avatar
Robert Czechowski committed
552
        Ok(session_token) => Ok(session_token),
Robert Czechowski's avatar
Robert Czechowski committed
553
554
        Err(()) => {
            let mut data = json_val::Map::new();
555
            data.insert("reason".to_string(), to_json(&"Login fehlgeschlagen. Bitte erneut versuchen.".to_string()));
Robert Czechowski's avatar
Robert Czechowski committed
556
            data.insert("username".to_string(), to_json(&username));
557
558
            data.insert("parent".to_string(), to_json(&"base"));

559
            fill_oauth_data(login_info, &mut data);
560

561
            Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
562
563
564
565
        }
    }
}

Robert Czechowski's avatar
Robert Czechowski committed
566
pub fn login_with_code<T: MedalConnection>(
567
    conn: &T, code: &str, login_info: LoginInfo)
Robert Czechowski's avatar
Robert Czechowski committed
568
    -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
569
    match conn.login_with_code(None, &code) {
Robert Czechowski's avatar
Robert Czechowski committed
570
571
572
573
574
575
576
        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));
577
578
                data.insert("parent".to_string(), to_json(&"base"));

579
                fill_oauth_data(login_info, &mut data);
580

Robert Czechowski's avatar
Robert Czechowski committed
581
                Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
582
            }
Robert Czechowski's avatar
Robert Czechowski committed
583
        },
Robert Czechowski's avatar
Robert Czechowski committed
584
585
586
    }
}

587
pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
588
    session_token.map(|token| conn.logout(&token));
Robert Czechowski's avatar
Robert Czechowski committed
589
590
}

591
#[cfg(feature = "signup")]
Robert Czechowski's avatar
Robert Czechowski committed
592
593
pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signup_data: (String, String, String))
                                  -> MedalResult<SignupResult> {
594
595
596
    let (username, email, password) = signup_data;

    if username == "" || email == "" || password == "" {
Robert Czechowski's avatar
Robert Czechowski committed
597
        return Ok(SignupResult::EmptyFields);
598
599
600
601
602
603
604
605
606
    }

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

607
#[cfg(feature = "signup")]
Robert Czechowski's avatar
Robert Czechowski committed
608
pub fn signupdata(query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
609
610
    let mut data = json_val::Map::new();
    if let Some(query) = query_string {
Robert Czechowski's avatar
Robert Czechowski committed
611
        if let Some(status) = query.strip_prefix("status=") {
612
613
614
615
616
617
618
619
            if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) {
                data.insert((status).to_string(), to_json(&true));
            }
        }
    }
    data
}

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

624
    match match subtask {
Robert Czechowski's avatar
Robert Czechowski committed
625
626
627
              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
628
        Some(submission) => Ok(submission.value),
Robert Czechowski's avatar
Robert Czechowski committed
629
        None => Ok("{}".to_string()),
Robert Czechowski's avatar
Robert Czechowski committed
630
631
632
    }
}

633
pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, csrf_token: &str,
634
                                           data: String, grade_percentage: i32, subtask: Option<String>)
Robert Czechowski's avatar
Robert Czechowski committed
635
636
                                           -> MedalResult<String>
{
637
    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
638
639

    if session.csrf_token != csrf_token {
640
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
641
642
    }

643
    let (t, _, c) = conn.get_task_by_id_complete(task_id);
644

645
    match conn.get_participation(session.id, c.id.expect("Value from database")) {
646
647
648
649
650
651
652
653
654
        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;
655
            if c.duration > 0 && left_secs < -10 {
656
657
658
                return Err(MedalError::AccessDenied);
                // Contest over
                // TODO: Nicer message!
659
660
661
662
            }
        }
    }

663
664
    /* 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
665
666
667
668
669
     * 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.
     */
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695

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

Robert Czechowski's avatar
Robert Czechowski committed
697
698
699
    let submission = Submission { id: None,
                                  session_user: session.id,
                                  task: task_id,
700
                                  grade: grade_rounded,
Robert Czechowski's avatar
Robert Czechowski committed
701
                                  validated: false,
702
                                  nonvalidated_grade: grade_rounded,
Robert Czechowski's avatar
Robert Czechowski committed
703
704
705
706
                                  needs_validation: true,
                                  subtask_identifier: subtask,
                                  value: data,
                                  date: time::get_time() };
707

Robert Czechowski's avatar
Robert Czechowski committed
708
709
710
711
712
    conn.submit_submission(submission);

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

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

716
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
717
718
    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?
719

720
721
722
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
723

724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
    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);
        }
    }
739

740
    match conn.get_own_participation(&session_token, c.id.expect("Value from database")) {
741
        None => Err(MedalError::AccessDenied),
742
743
744
745
746
747
        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
748

749
            let mut data = json_val::Map::new();
750
            data.insert("participation_start_date".to_string(), to_json(&format!("{}", passed_secs)));
751
752
753
            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
754

755
            let left_secs = i64::from(c.duration) * 60 - passed_secs;
756
            if c.duration > 0 && left_secs < 0 {
757
                Err(MedalError::AccessDenied)
758
759
760
761
762
            // 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
763
764
                data.insert("time_left".to_string(), to_json(&format!("{}:{:02}", hour, min)));
                data.insert("time_left_sec".to_string(), to_json(&format!(":{:02}", sec)));
765
766

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

768
                data.insert("contestname".to_string(), to_json(&c.name));
769
                data.insert("name".to_string(), to_json(&tg.name));
770
                data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &c.name)));
771
                data.insert("taskid".to_string(), to_json(&task_id));
772
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
773
774
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
775
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
776

Robert Czechowski's avatar
Robert Czechowski committed
777
778
779
780
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

781
782
783
784
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
785
}
Robert Czechowski's avatar
Robert Czechowski committed
786

Robert Czechowski's avatar
Robert Czechowski committed
787
788
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
789
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
790
791
792
793
794
    pub name: String,
    pub tag: String,
    pub code: String,
}

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

Robert Czechowski's avatar
Robert Czechowski committed
798
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
799
800

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

Robert Czechowski's avatar
Robert Czechowski committed
803
804
805
806
807
808
809
810
    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
811
    data.insert("group".to_string(), to_json(&v));
812
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
813

814
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
815
816
}

Robert Czechowski's avatar
Robert Czechowski committed
817
818
#[derive(Serialize, Deserialize)]
pub struct MemberInfo {
819
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
820
821
    pub firstname: String,
    pub lastname: String,
822
    pub grade: String,
Robert Czechowski's avatar
Robert Czechowski committed
823
824
825
    pub logincode: String,
}

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

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

Robert Czechowski's avatar
Robert Czechowski committed
833
    if group.admin != session.id {
834
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
835
836
    }

Robert Czechowski's avatar
Robert Czechowski committed
837
838
839
840
841
842
843
844
845
846
847
    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()),
848
                                   grade: grade_to_string(m.grade),
Robert Czechowski's avatar
Robert Czechowski committed
849
850
                                   logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()) })
             .collect();
Robert Czechowski's avatar
Robert Czechowski committed
851
852
853

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
854
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
855
856
857

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

859
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
860
861
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
862

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

    if session.csrf_token != csrf_token {
868
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
869
870
    }

Robert Czechowski's avatar
Robert Czechowski committed
871
    let groupcode = helpers::make_group_code();
872
    // TODO: check for collisions
873

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

Robert Czechowski's avatar
Robert Czechowski committed
876
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
877
878
879

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

881
pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str, sex_infos: SexInformation) -> MedalValueResult {
882
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
883
884
885
886

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

887
888
889
890
891
    data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
    data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
    data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
    data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));

892
893
894
    Ok(("groupcsv".to_string(), data))
}

895
// TODO: Should creating the users and groups happen in a batch operation to speed things up?
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
896
897
pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
                                         -> MedalResult<()> {
898
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
899
900
901
902
903

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

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

906
907
908
909
910
    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
911
912
913
914
915
916
917
    let mut group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: "".to_string(),
                            admin: session.id,
                            members: Vec::new() };

918
919
    for line in v {
        if name != line[0] {
920
            if name != "" {
921
                conn.create_group_with_users(group);
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
922
            }
923
924
925
926
            name = line[0].clone();
            group_code = helpers::make_group_code();
            // TODO: check for collisions

Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
927
928
929
930
931
932
            group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: name.clone(),
                            admin: session.id,
                            members: Vec::new() };
933
934
935
936
937
938
939
        }

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

940
941
942
943
944
945
946
947
        use db_objects::Sex;
        match line[4].as_str() {
            "m" => user.sex = Some(Sex::Male as i32),
            "f" => user.sex = Some(Sex::Female as i32),
            "d" => user.sex = Some(Sex::Diverse as i32),
            _ => user.sex = None,
        }

948
949
950
951
        group.members.push(user);
    }
    conn.create_group_with_users(group);

952
953
954
    Ok(())
}

Daniel Brüning's avatar
Daniel Brüning committed
955
#[allow(dead_code)]
956
pub fn show_groups_results<T: MedalConnection>(conn: &T, contest_id: