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

15
16
use time;

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

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

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

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

49
#[derive(Clone, Debug)]
50
pub enum MedalError {
51
52
    NotLoggedIn,
    AccessDenied,
53
54
55
    CsrfCheckFailed,
    SessionTimeout,
    DatabaseError,
56
    DatabaseConnectionError,
57
    PasswordHashingError,
58
    UnmatchedPasswords,
59
    NotFound,
60
    OauthError(String),
61
}
62

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

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

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

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

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

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

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

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

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

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

131
    fill_oauth_data(login_info, &mut data);
132

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

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

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

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

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

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

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

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

218
    fill_oauth_data(login_info, &mut data);
219

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

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

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

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

        subtaskinfos.push(si);
    }
    subtaskinfos
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

528
            fill_oauth_data(login_info, &mut data);
529

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

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

548
                fill_oauth_data(login_info, &mut data);
549

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

908
909
910
    Ok(())
}

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

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

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

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

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

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

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

943
944
    match user_id {
        None => {
945
946
947
948
949
            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));
950
            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
951
952
953
954
955
            if let Some(sex) = session.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
956

957
            data.insert("profile_logincode".to_string(), to_json(&session.logincode));
958
            if session.password.is_some() {
959
                data.insert("profile_username".to_string(), to_json(&session.username));