core.rs 60 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
85
    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));
86
    data.insert("parent".to_string(), to_json(&"base"));
87
88

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

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

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

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

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

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

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

132
    fill_oauth_data(login_info, &mut data);
133

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

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

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

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

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

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

212
    let session = conn.get_session_or_new(&session_token).map_err(|_| MedalError::DatabaseConnectionError)?;
213
    fill_user_data(&session, &mut data);
214

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

219
    fill_oauth_data(login_info, &mut data);
220

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

247
    Ok(("contests".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
248
249
}

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

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

        subtaskinfos.push(si);
    }
    subtaskinfos
}

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

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

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

313
    let mut opt_part = conn.get_participation(session.id, contest_id);
314

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

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

327
    let constraints = check_contest_constraints(&session, &contest);
328

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

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

336
337
338
339
    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));

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

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

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

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

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

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

409
        data.insert("is_started".to_string(), to_json(&true));
410
        data.insert("participation_start_date".to_string(), to_json(&passed_secs));
411
412
        data.insert("total_points".to_string(), to_json(&totalgrade));
        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
413
414
415
        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));
416
417
418
419
420
421
        if is_time_left {
            data.insert("left_min".to_string(), to_json(&left_min));
            data.insert("left_sec".to_string(), to_json(&left_sec));
            data.insert("time_left".to_string(), to_json(&time_left));
            data.insert("seconds_left".to_string(), to_json(&left_secs));
        }
Robert Czechowski's avatar
Robert Czechowski committed
422
    }
423

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

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

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

440
441
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

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

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

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

451
452
453
            userresults.push(String::new());
            let mut summe = 0;

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

463
464
            userresults[0] = format!("{}", summe);

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

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

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
477
478

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

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

484
485
486
    Ok(("contestresults".to_owned(), data))
}

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

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

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

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

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

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

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

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

529
            fill_oauth_data(login_info, &mut data);
530

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

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

549
                fill_oauth_data(login_info, &mut data);
550

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

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

561
#[cfg(feature = "signup")]
Robert Czechowski's avatar
Robert Czechowski committed
562
563
pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signup_data: (String, String, String))
                                  -> MedalResult<SignupResult> {
564
565
566
    let (username, email, password) = signup_data;

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

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

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

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

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

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

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

614
    let (t, _, c) = conn.get_task_by_id_complete(task_id);
615

616
    match conn.get_participation(session.id, c.id.expect("Value from database")) {
617
618
619
620
621
622
623
624
625
        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;
626
            if c.duration > 0 && left_secs < -10 {
627
628
629
                return Err(MedalError::AccessDenied);
                // Contest over
                // TODO: Nicer message!
630
631
632
633
            }
        }
    }

634
635
    /* 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
636
637
638
639
640
     * 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.
     */
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

909
910
911
    Ok(())
}

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

918
    let data = json_val::Map::new();
919
920
921

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

923
924
925
926
927
928
929
pub struct SexInformation {
    pub require_sex: bool,
    pub allow_sex_na: bool,
    pub allow_sex_diverse: bool,
    pub allow_sex_other: bool,
}

930
pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
931
                                        query_string: Option<String>, sex_infos: SexInformation)
Robert Czechowski's avatar
Robert Czechowski committed
932
933
                                        -> MedalValueResult
{
934
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
935
936

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

939
940
941
942
943
    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));

944
945
    match user_id {
        None => {
946
947
948
949
950
            data.insert("profile_firstname".to_string(), to_json(&session.firstname));
            data.insert("profile_lastname".to_string(), to_json(&session.lastname));
            data.insert("profile_street".to_string(), to_json(&session.street));
            data.insert("profile_zip".to_string(), to_json(&session.zip));
            data.insert("profile_city".to_string(), to_json(&session.city));
951
            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
952
953
954
955
956
            if let Some(sex) = session.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
957

958
            data.insert("profile_logincode".to_string(), to_json(&session.logincode));
959
            if session.password.is_some() {
Robert Czechowski's avatar