core.rs 60.3 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
pub fn status<T: MedalConnection>(conn: &T, _: ()) -> String { conn.get_debug_information() }
156

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

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

pub fn debug_create_session<T: MedalConnection>(conn: &T, session_token: Option<String>) {
    if let Some(token) = session_token {
195
        conn.get_session_or_new(&token).unwrap();
196
197
198
    }
}

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

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

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

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

220
    fill_oauth_data(login_info, &mut data);
221

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

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

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

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

        subtaskinfos.push(si);
    }
    subtaskinfos
}

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
304
#[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 }
}

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

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

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

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

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

329
    let constraints = check_contest_constraints(&session, &contest);
330

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

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

338
339
340
341
    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));

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

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

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

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

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

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

411
        data.insert("is_started".to_string(), to_json(&true));
412
        data.insert("participation_start_date".to_string(), to_json(&passed_secs));
413
414
        data.insert("total_points".to_string(), to_json(&totalgrade));
        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
415
416
417
        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));
418
419
420
421
422
423
        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));
        }
424
        data.insert("no_login".to_string(), to_json(&true));
Robert Czechowski's avatar
Robert Czechowski committed
425
    }
426

427
428
429
430
431
432
    // 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));
433
434
    }

435
    Ok(("contest".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
436
437
}

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

443
444
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

445
    let mut results: Vec<(String, i32, Vec<(String, String, i32, String, Vec<String>)>)> = Vec::new();
446
447

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

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

454
455
456
            userresults.push(String::new());
            let mut summe = 0;

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

466
467
            userresults[0] = format!("{}", summe);

468
469
            groupresults.push((user.firstname.unwrap_or_else(|| "–".to_string()),
                               user.lastname.unwrap_or_else(|| "–".to_string()),
470
                               user.id,
471
                               grade_to_string(user.grade),
472
                               userresults))
473
        }
474

Robert Czechowski's avatar
Robert Czechowski committed
475
        results.push((group.name.to_string(), group.id.unwrap_or(0), groupresults));
476
477
478
479
    }

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
480
481

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

484
    data.insert("contest".to_string(), to_json(&ci));
485
    data.insert("contestname".to_string(), to_json(&c.name));
486

487
488
489
    Ok(("contestresults".to_owned(), data))
}

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

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

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

506
507
508
509
510
511
512
    // Check other constraints
    let constraints = check_contest_constraints(&session, &contest);

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

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

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

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

532
            fill_oauth_data(login_info, &mut data);
533

534
            Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
535
536
537
538
        }
    }
}

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

552
                fill_oauth_data(login_info, &mut data);
553

Robert Czechowski's avatar
Robert Czechowski committed
554
                Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
555
            }
Robert Czechowski's avatar
Robert Czechowski committed
556
        },
Robert Czechowski's avatar
Robert Czechowski committed
557
558
559
    }
}

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

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

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

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

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

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

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

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

    if session.csrf_token != csrf_token {
614
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
615
616
    }

617
    let (t, _, c) = conn.get_task_by_id_complete(task_id);
618

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

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

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

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

Robert Czechowski's avatar
Robert Czechowski committed
682
683
684
685
686
    conn.submit_submission(submission);

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

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

690
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
691
692
    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?
693

694
695
696
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
697

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

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

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

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

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

742
                data.insert("contestname".to_string(), to_json(&c.name));
743
                data.insert("name".to_string(), to_json(&tg.name));
744
                data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &c.name)));
745
                data.insert("taskid".to_string(), to_json(&task_id));
746
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
747
748
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
749
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
750

Robert Czechowski's avatar
Robert Czechowski committed
751
752
753
754
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

755
756
757
758
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
759
}
Robert Czechowski's avatar
Robert Czechowski committed
760

Robert Czechowski's avatar
Robert Czechowski committed
761
762
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
763
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
764
765
766
767
768
    pub name: String,
    pub tag: String,
    pub code: String,
}

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

Robert Czechowski's avatar
Robert Czechowski committed
772
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
773
774

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

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

788
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
789
790
}

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

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

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

Robert Czechowski's avatar
Robert Czechowski committed
807
    if group.admin != session.id {
808
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
809
810
    }

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

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
828
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
829
830
831

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

833
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
834
835
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
836

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

    if session.csrf_token != csrf_token {
842
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
843
844
    }

Robert Czechowski's avatar
Robert Czechowski committed
845
    let groupcode = helpers::make_group_code();
846
    // TODO: check for collisions
847

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

Robert Czechowski's avatar
Robert Czechowski committed
850
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
851
852
853

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

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

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

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

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

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

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

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

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

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

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

913
914
915
    Ok(())
}

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

922
    let data = json_val::Map::new();
923
924
925

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

927
928
929
930
931
932
933
pub struct SexInformation {
    pub require_sex: bool,
    pub allow_sex_na: bool,
    pub allow_sex_diverse: bool,
    pub allow_sex_other: bool,
}

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

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

943
944
945
946
947
    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));

948
949
    match user_id {
        None => {
950
951
952
953
954
            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));