core.rs 60.9 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
#[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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#[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 }
}

311
pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
312
                                        query_string: Option<String>, login_info: LoginInfo)
313
314
                                        -> MedalValueResult
{
315
    let session = conn.get_session_or_new(&session_token).unwrap();
316

317
    let contest = conn.get_contest_by_id_complete(contest_id);
318
    let grades = conn.get_contest_user_grades(&session_token, contest_id);
319

320
    let mut opt_part = conn.get_participation(session.id, contest_id);
321

322
323
324
    let ci = ContestInfo { id: contest.id.unwrap(),
                           name: contest.name.clone(),
                           duration: contest.duration,
325
                           public: contest.public };
Robert Czechowski's avatar
Robert Czechowski committed
326
327

    let mut data = json_val::Map::new();
328
329
    data.insert("parent".to_string(), to_json(&"base"));
    data.insert("empty".to_string(), to_json(&"empty"));
Robert Czechowski's avatar
Robert Czechowski committed
330
    data.insert("contest".to_string(), to_json(&ci));
331
    data.insert("title".to_string(), to_json(&ci.name));
332
    data.insert("message".to_string(), to_json(&contest.message));
333
    fill_oauth_data(login_info, &mut data);
Robert Czechowski's avatar
Robert Czechowski committed
334

335
    let constraints = check_contest_constraints(&session, &contest);
336

337
    let can_start = session.is_logged_in() && constraints.contest_running && constraints.grade_matching;
338
    let has_duration = contest.duration > 0;
339

340
    data.insert("constraints".to_string(), to_json(&constraints));
341
    data.insert("has_duration".to_string(), to_json(&has_duration));
342
    data.insert("can_start".to_string(), to_json(&can_start));
343

344
345
346
347
    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));

348
349
350
351
    // 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) {
352
353
354
355
356
357
    if opt_part.is_none()
       && contest.duration == 0
       && constraints.contest_running
       && constraints.grade_matching
       && contest.requires_login != Some(true)
    {
358
359
360
361
        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() });
    }

362
    let now = time::get_time();
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
    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));
386
387
    }

388
389
390
    if let Some(participation) = opt_part {
        let mut totalgrade = 0;
        let mut max_totalgrade = 0;
391

392
        let mut tasks = Vec::new();
393
        for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
394
395
396
397
398
399
400
            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);
        }
401
        let relative_points = (totalgrade * 100) / max_totalgrade;
402
403

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

405
406
407
408
409
        let now = time::get_time();
        let passed_secs = now.sec - participation.start.sec;
        if passed_secs < 0 {
            // behandle inkonsistente Serverzeit
        }
410
411
        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
        let is_time_left = contest.duration == 0 || left_secs >= 0;
412
413
414
415
        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);
416

417
        data.insert("is_started".to_string(), to_json(&true));
418
        data.insert("participation_start_date".to_string(), to_json(&passed_secs));
419
420
        data.insert("total_points".to_string(), to_json(&totalgrade));
        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
421
422
423
        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));
424
425
426
427
428
429
        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));
        }
430
        data.insert("no_login".to_string(), to_json(&true));
Robert Czechowski's avatar
Robert Czechowski committed
431
    }
432

433
434
435
436
437
438
    // 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));
439
440
    }

441
    Ok(("contest".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
442
443
}

444
pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
445
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
446
447
    let mut data = json_val::Map::new();
    fill_user_data(&session, &mut data);
448

449
450
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

451
    let mut results: Vec<(String, i32, Vec<(String, String, i32, String, Vec<String>)>)> = Vec::new();
452
453

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

Daniel Brüning's avatar
Daniel Brüning committed
456
        //TODO: use user
457
        for (user, userdata) in groupdata {
458
459
            let mut userresults: Vec<String> = Vec::new();

460
461
462
            userresults.push(String::new());
            let mut summe = 0;

463
            for grade in userdata {
464
465
466
467
                if let Some(g) = grade.grade {
                    userresults.push(format!("{}", g));
                    summe += g;
                } else {
Robert Czechowski's avatar
Robert Czechowski committed
468
                    userresults.push("–".to_string());
469
                }
470
471
            }

472
473
            userresults[0] = format!("{}", summe);

474
475
            groupresults.push((user.firstname.unwrap_or_else(|| "–".to_string()),
                               user.lastname.unwrap_or_else(|| "–".to_string()),
476
                               user.id,
477
                               grade_to_string(user.grade),
478
                               userresults))
479
        }
480

Robert Czechowski's avatar
Robert Czechowski committed
481
        results.push((group.name.to_string(), group.id.unwrap_or(0), groupresults));
482
483
484
485
    }

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
486
487

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

490
    data.insert("contest".to_string(), to_json(&ci));
491
    data.insert("contestname".to_string(), to_json(&c.name));
492

493
494
495
    Ok(("contestresults".to_owned(), data))
}

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

502
    // Check logged in or open contest
503
    if contest.duration != 0 && !session.is_logged_in() {
504
        return Err(MedalError::AccessDenied);
505
    }
506

507
    // Check CSRF token
508
509
510
    if session.is_logged_in() && session.csrf_token != csrf_token {
        return Err(MedalError::CsrfCheckFailed);
    }
511

512
513
514
515
516
517
518
    // Check other constraints
    let constraints = check_contest_constraints(&session, &contest);

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

519
    // Start contest
520
    match conn.new_participation(&session_token, contest_id) {
Robert Czechowski's avatar
Robert Czechowski committed
521
        Ok(_) => Ok(()),
522
        _ => Err(MedalError::AccessDenied), // Contest already started TODO: Maybe redirect to page with hint
Robert Czechowski's avatar
Robert Czechowski committed
523
    }
Robert Czechowski's avatar
Robert Czechowski committed
524
525
}

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

530
    match conn.login(None, &username, &password) {
Robert Czechowski's avatar
Robert Czechowski committed
531
        Ok(session_token) => Ok(session_token),
Robert Czechowski's avatar
Robert Czechowski committed
532
533
        Err(()) => {
            let mut data = json_val::Map::new();
534
            data.insert("reason".to_string(), to_json(&"Login fehlgeschlagen. Bitte erneut versuchen.".to_string()));
Robert Czechowski's avatar
Robert Czechowski committed
535
            data.insert("username".to_string(), to_json(&username));
536
537
            data.insert("parent".to_string(), to_json(&"base"));

538
            fill_oauth_data(login_info, &mut data);
539

540
            Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
541
542
543
544
        }
    }
}

Robert Czechowski's avatar
Robert Czechowski committed
545
pub fn login_with_code<T: MedalConnection>(
546
    conn: &T, code: &str, login_info: LoginInfo)
Robert Czechowski's avatar
Robert Czechowski committed
547
    -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
548
    match conn.login_with_code(None, &code) {
Robert Czechowski's avatar
Robert Czechowski committed
549
550
551
552
553
554
555
        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));
556
557
                data.insert("parent".to_string(), to_json(&"base"));

558
                fill_oauth_data(login_info, &mut data);
559

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

566
pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
567
    session_token.map(|token| conn.logout(&token));
Robert Czechowski's avatar
Robert Czechowski committed
568
569
}

570
#[cfg(feature = "signup")]
Robert Czechowski's avatar
Robert Czechowski committed
571
572
pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signup_data: (String, String, String))
                                  -> MedalResult<SignupResult> {
573
574
575
    let (username, email, password) = signup_data;

    if username == "" || email == "" || password == "" {
Robert Czechowski's avatar
Robert Czechowski committed
576
        return Ok(SignupResult::EmptyFields);
577
578
579
580
581
582
583
584
585
    }

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

586
#[cfg(feature = "signup")]
Robert Czechowski's avatar
Robert Czechowski committed
587
pub fn signupdata(query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
588
589
590
591
592
593
594
595
596
597
598
599
    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
}

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

604
    match match subtask {
Robert Czechowski's avatar
Robert Czechowski committed
605
606
607
              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
608
        Some(submission) => Ok(submission.value),
Robert Czechowski's avatar
Robert Czechowski committed
609
        None => Ok("{}".to_string()),
Robert Czechowski's avatar
Robert Czechowski committed
610
611
612
    }
}

613
pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, csrf_token: &str,
614
                                           data: String, grade_percentage: i32, subtask: Option<String>)
Robert Czechowski's avatar
Robert Czechowski committed
615
616
                                           -> MedalResult<String>
{
617
    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
618
619

    if session.csrf_token != csrf_token {
620
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
621
622
    }

623
    let (t, _, c) = conn.get_task_by_id_complete(task_id);
624

625
    match conn.get_participation(session.id, c.id.expect("Value from database")) {
626
627
628
629
630
631
632
633
634
        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;
635
            if c.duration > 0 && left_secs < -10 {
636
637
638
                return Err(MedalError::AccessDenied);
                // Contest over
                // TODO: Nicer message!
639
640
641
642
            }
        }
    }

643
644
    /* 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
645
646
647
648
649
     * 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.
     */
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675

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

Robert Czechowski's avatar
Robert Czechowski committed
677
678
679
    let submission = Submission { id: None,
                                  session_user: session.id,
                                  task: task_id,
680
                                  grade: grade_rounded,
Robert Czechowski's avatar
Robert Czechowski committed
681
                                  validated: false,
682
                                  nonvalidated_grade: grade_rounded,
Robert Czechowski's avatar
Robert Czechowski committed
683
684
685
686
                                  needs_validation: true,
                                  subtask_identifier: subtask,
                                  value: data,
                                  date: time::get_time() };
687

Robert Czechowski's avatar
Robert Czechowski committed
688
689
690
691
692
    conn.submit_submission(submission);

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

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

696
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
697
698
    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?
699

700
701
702
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
703

704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
    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);
        }
    }
719

720
    match conn.get_own_participation(&session_token, c.id.expect("Value from database")) {
721
        None => Err(MedalError::AccessDenied),
722
723
724
725
726
727
        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
728

729
            let mut data = json_val::Map::new();
730
            data.insert("participation_start_date".to_string(), to_json(&format!("{}", passed_secs)));
731
732
733
            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
734

735
            let left_secs = i64::from(c.duration) * 60 - passed_secs;
736
            if c.duration > 0 && left_secs < 0 {
737
                Err(MedalError::AccessDenied)
738
739
740
741
742
            // 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
743
744
                data.insert("time_left".to_string(), to_json(&format!("{}:{:02}", hour, min)));
                data.insert("time_left_sec".to_string(), to_json(&format!(":{:02}", sec)));
745
746

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

748
                data.insert("contestname".to_string(), to_json(&c.name));
749
                data.insert("name".to_string(), to_json(&tg.name));
750
                data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &c.name)));
751
                data.insert("taskid".to_string(), to_json(&task_id));
752
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
753
754
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
755
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
756

Robert Czechowski's avatar
Robert Czechowski committed
757
758
759
760
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

761
762
763
764
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
765
}
Robert Czechowski's avatar
Robert Czechowski committed
766

Robert Czechowski's avatar
Robert Czechowski committed
767
768
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
769
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
770
771
772
773
774
    pub name: String,
    pub tag: String,
    pub code: String,
}

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

Robert Czechowski's avatar
Robert Czechowski committed
778
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
779
780

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

Robert Czechowski's avatar
Robert Czechowski committed
783
784
785
786
787
788
789
790
    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
791
    data.insert("group".to_string(), to_json(&v));
792
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
793

794
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
795
796
}

Robert Czechowski's avatar
Robert Czechowski committed
797
798
#[derive(Serialize, Deserialize)]
pub struct MemberInfo {
799
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
800
801
    pub firstname: String,
    pub lastname: String,
802
    pub grade: String,
Robert Czechowski's avatar
Robert Czechowski committed
803
804
805
    pub logincode: String,
}

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

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

Robert Czechowski's avatar
Robert Czechowski committed
813
    if group.admin != session.id {
814
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
815
816
    }

Robert Czechowski's avatar
Robert Czechowski committed
817
818
819
820
821
822
823
824
825
826
827
    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()),
828
                                   grade: grade_to_string(m.grade),
Robert Czechowski's avatar
Robert Czechowski committed
829
830
                                   logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()) })
             .collect();
Robert Czechowski's avatar
Robert Czechowski committed
831
832
833

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
834
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
835
836
837

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

839
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
840
841
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
842

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

    if session.csrf_token != csrf_token {
848
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
849
850
    }

Robert Czechowski's avatar
Robert Czechowski committed
851
    let groupcode = helpers::make_group_code();
852
    // TODO: check for collisions
853

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

Robert Czechowski's avatar
Robert Czechowski committed
856
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
857
858
859

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

861
pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
862
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
863
864
865
866
867
868
869

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

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

870
// TODO: Should creating the users and groups happen in a batch operation to speed things up?
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
871
872
pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
                                         -> MedalResult<()> {
873
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
874
875
876
877
878

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

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

881
882
883
884
885
    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
886
887
888
889
890
891
892
    let mut group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: "".to_string(),
                            admin: session.id,
                            members: Vec::new() };

893
894
    for line in v {
        if name != line[0] {
895
            if name != "" {
896
                conn.create_group_with_users(group);
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
897
            }
898
899
900
901
            name = line[0].clone();
            group_code = helpers::make_group_code();
            // TODO: check for collisions

Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
902
903
904
905
906
907
            group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: name.clone(),
                            admin: session.id,
                            members: Vec::new() };
908
909
910
911
912
913
914
915
916
917
918
        }

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

919
920
921
    Ok(())
}

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

928
    let data = json_val::Map::new();
929
930
931

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

933
934
935
936
937
938
939
pub struct SexInformation {
    pub require_sex: bool,
    pub allow_sex_na: bool,
    pub allow_sex_diverse: bool,
    pub allow_sex_other: bool,
}

940
pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
941
                                        query_string: Option<String>, sex_infos: SexInformation)
Robert Czechowski's avatar
Robert Czechowski committed
942
943
                                        -> MedalValueResult
{
944
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
945
946

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

949
950
951
952
953
    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));

954
955
    match user_id {
        None => {
956
957
958
959
960
            data.insert("profile_firstname".to_string