core.rs 60.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*  medal                                                                                                            *\
 *  Copyright (C) 2020  Bundesweite Informatikwettbewerbe                                                            *
 *                                                                                                                   *
 *  This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero        *
 *  General Public License as published  by the Free Software Foundation, either version 3 of the License, or (at    *
 *  your option) any later version.                                                                                  *
 *                                                                                                                   *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the       *
 *  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public      *
 *  License for more details.                                                                                        *
 *                                                                                                                   *
 *  You should have received a copy of the GNU Affero General Public License along with this program.  If not, see   *
\*  <http://www.gnu.org/licenses/>.                                                                                  */

15
16
use time;

Robert Czechowski's avatar
Robert Czechowski committed
17
use db_conn::MedalConnection;
18
19
#[cfg(feature = "signup")]
use db_conn::SignupResult;
20
use db_objects::OptionSession;
21
use db_objects::SessionUser;
22
use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
23
24
25
use helpers;
use oauth_provider::OauthProvider;
use webfw_iron::{json_val, to_json};
26

27
28
#[derive(Serialize, Deserialize)]
pub struct SubTaskInfo {
29
    pub id: i32,
30
    pub linktext: String,
31
32
    pub active: bool,
    pub greyout: bool,
33
34
35
36
37
38
39
40
}

#[derive(Serialize, Deserialize)]
pub struct TaskInfo {
    pub name: String,
    pub subtasks: Vec<SubTaskInfo>,
}

Robert Czechowski's avatar
Robert Czechowski committed
41
42
#[derive(Serialize, Deserialize)]
pub struct ContestInfo {
43
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
44
    pub name: String,
45
    pub duration: i32,
Robert Czechowski's avatar
Robert Czechowski committed
46
47
48
    pub public: bool,
}

49
#[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("title".to_string(), to_json(&ci.name));
325
    data.insert("message".to_string(), to_json(&contest.message));
326
    fill_oauth_data(login_info, &mut data);
Robert Czechowski's avatar
Robert Czechowski committed
327

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

531
            fill_oauth_data(login_info, &mut data);
532

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

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

551
                fill_oauth_data(login_info, &mut data);
552

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

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

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

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

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

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

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

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

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

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

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

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

636
637
    /* 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
638
639
640
641
642
     * 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.
     */
643
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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

912
913
914
    Ok(())
}

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

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

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

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

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

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

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

947
948
    match user_id {
        None => {
949
950
951
952
953
            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));
954
            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
955
956