core.rs 56.7 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
use db_objects::OptionSession;
19
use db_objects::SessionUser;
20
use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
21
22
23
use helpers;
use oauth_provider::OauthProvider;
use webfw_iron::{json_val, to_json};
24

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

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

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

47
48
#[derive(Clone)]
pub enum MedalError {
49
50
    NotLoggedIn,
    AccessDenied,
51
52
53
    CsrfCheckFailed,
    SessionTimeout,
    DatabaseError,
54
    PasswordHashingError,
55
    UnmatchedPasswords,
56
}
57

58
59
60
61
type MedalValue = (String, json_val::Map<String, json_val::Value>);
type MedalResult<T> = Result<T, MedalError>;
type MedalValueResult = MedalResult<MedalValue>;

62
63
64
65
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));
    }
66
67
68
    if session.is_admin() {
        data.insert("admin".to_string(), to_json(&true));
    }
69
70
71
72
73
    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));
74
    data.insert("parent".to_string(), to_json(&"base"));
75
76

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

79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
fn fill_oauth_data((self_url, oauth_providers): (Option<String>, Option<Vec<OauthProvider>>),
                   data: &mut json_val::Map<String, serde_json::Value>)
{
    let mut oauth_links: Vec<(String, String, String)> = Vec::new();
    if let Some(oauth_providers) = oauth_providers {
        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()));
        }
    }

    data.insert("self_url".to_string(), to_json(&self_url));
    data.insert("oauth_links".to_string(), to_json(&oauth_links));
}

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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(),
    }
}

Robert Czechowski's avatar
Robert Czechowski committed
110
pub fn index<T: MedalConnection>(conn: &T, session_token: Option<String>,
111
                                 oauth_infos: (Option<String>, Option<Vec<OauthProvider>>))
Robert Czechowski's avatar
Robert Czechowski committed
112
113
                                 -> (String, json_val::Map<String, json_val::Value>)
{
114
115
116
117
118
119
    let mut data = json_val::Map::new();

    //let mut contests = Vec::new();

    if let Some(token) = session_token {
        if let Some(session) = conn.get_session(&token) {
120
            fill_user_data(&session, &mut data);
121
122
123
        }
    }

124
    fill_oauth_data(oauth_infos, &mut data);
125

126
    data.insert("parent".to_string(), to_json(&"base"));
127
128
129
    ("index".to_owned(), data)
}

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

132
133
134
135
136
137
138
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));
139
            data.insert("session_id".to_string(), to_json(&session.id));
140
141
142
143
144
145
146
147
148
149
150
151
152
            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));
153
154
                    data.insert("oauth_provider".to_string(), to_json(&session.oauth_provider));
                    data.insert("oauth_id".to_string(), to_json(&session.oauth_foreign_id));
155
156
                    data.insert("logincode".to_string(), to_json(&session.logincode));
                    data.insert("managed_by".to_string(), to_json(&session.managed_by));
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
                }
            }
        }
        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 {
        conn.get_session_or_new(&token);
    }
}

174
175
176
177
#[derive(PartialEq, Eq)]
pub enum ContestVisibility {
    All,
    Open,
178
    Current,
179
    LoginRequired,
180
181
}

182
pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str,
183
                                         oauth_infos: (Option<String>, Option<Vec<OauthProvider>>),
184
185
186
                                         visibility: ContestVisibility)
                                         -> MedalValue
{
Robert Czechowski's avatar
Robert Czechowski committed
187
188
    let mut data = json_val::Map::new();

189
    let session = conn.get_session_or_new(&session_token);
190
    fill_user_data(&session, &mut data);
191

192
193
194
195
    if session.is_logged_in() {
        data.insert("can_start".to_string(), to_json(&true));
    }

196
    fill_oauth_data(oauth_infos, &mut data);
197

198
199
200
201
202
203
204
205
206
    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)
207
208
209
210
211
            .filter(|c| {
                !c.requires_login.unwrap_or(false)
                || visibility == ContestVisibility::LoginRequired
                || visibility == ContestVisibility::All
            })
212
213
214
            .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
215
    data.insert("contest".to_string(), to_json(&v));
216
217
    data.insert("contestlist_header".to_string(),
                to_json(&match visibility {
218
                            ContestVisibility::Open => "Trainingsaufgaben",
219
                            ContestVisibility::Current => "Aktuelle Wettbewerbe",
220
                            ContestVisibility::LoginRequired => "Herausforderungen",
221
222
                            ContestVisibility::All => "Alle Wettbewerbe",
                        }));
Robert Czechowski's avatar
Robert Czechowski committed
223
224
225
226

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

227
fn generate_subtaskstars(tg: &Taskgroup, grade: &Grade, ast: Option<i32>) -> Vec<SubTaskInfo> {
228
229
    let mut subtaskinfos = Vec::new();
    let mut not_print_yet = true;
230
    for st in &tg.tasks {
231
232
233
234
235
236
        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;
        }

237
238
239
240
241
        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
242
        let si = SubTaskInfo { id: st.id.unwrap(), linktext, active, greyout };
243
244
245
246
247
248

        subtaskinfos.push(si);
    }
    subtaskinfos
}

249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#[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 }
}

281
282
283
284
285
pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
                                        query_string: Option<String>,
                                        oauth_infos: (Option<String>, Option<Vec<OauthProvider>>))
                                        -> MedalValueResult
{
286
287
    let session = conn.get_session_or_new(&session_token);

288
    let contest = conn.get_contest_by_id_complete(contest_id);
289
    let grades = conn.get_contest_user_grades(&session_token, contest_id);
290

291
    let mut opt_part = conn.get_participation(&session_token, contest_id);
292

293
294
295
    let ci = ContestInfo { id: contest.id.unwrap(),
                           name: contest.name.clone(),
                           duration: contest.duration,
296
                           public: contest.public };
Robert Czechowski's avatar
Robert Czechowski committed
297
298

    let mut data = json_val::Map::new();
299
300
    data.insert("parent".to_string(), to_json(&"base"));
    data.insert("empty".to_string(), to_json(&"empty"));
Robert Czechowski's avatar
Robert Czechowski committed
301
    data.insert("contest".to_string(), to_json(&ci));
302
    data.insert("message".to_string(), to_json(&contest.message));
303
    fill_oauth_data(oauth_infos, &mut data);
Robert Czechowski's avatar
Robert Czechowski committed
304

305
    let constraints = check_contest_constraints(&session, &contest);
306

307
    let can_start = session.is_logged_in() && constraints.contest_running && constraints.grade_matching;
308
    let has_duration = contest.duration > 0;
309

310
    data.insert("constraints".to_string(), to_json(&constraints));
311
    data.insert("has_duration".to_string(), to_json(&has_duration));
312
    data.insert("can_start".to_string(), to_json(&can_start));
313

314
315
316
317
    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));

318
319
320
321
    // 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) {
322
323
324
325
326
327
    if opt_part.is_none()
       && contest.duration == 0
       && constraints.contest_running
       && constraints.grade_matching
       && contest.requires_login != Some(true)
    {
328
329
330
331
        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() });
    }

332
    let now = time::get_time();
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
    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));
356
357
    }

358
359
360
    if let Some(participation) = opt_part {
        let mut totalgrade = 0;
        let mut max_totalgrade = 0;
361

362
        let mut tasks = Vec::new();
363
        for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
364
365
366
367
368
369
370
            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);
        }
371
        let relative_points = (totalgrade * 100) / max_totalgrade;
372
373

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

375
376
377
378
379
        let now = time::get_time();
        let passed_secs = now.sec - participation.start.sec;
        if passed_secs < 0 {
            // behandle inkonsistente Serverzeit
        }
380
381
        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
        let is_time_left = contest.duration == 0 || left_secs >= 0;
382
383
384
385
        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);
386

387
        data.insert("is_started".to_string(), to_json(&true));
388
        data.insert("participation_start_date".to_string(), to_json(&passed_secs));
389
390
        data.insert("total_points".to_string(), to_json(&totalgrade));
        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
391
392
393
        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));
394
395
396
397
398
399
        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
400
    }
401

402
403
404
405
406
407
    // 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));
408
409
    }

410
    Ok(("contest".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
411
412
}

413
pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
414
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
415
416
    let mut data = json_val::Map::new();
    fill_user_data(&session, &mut data);
417

418
419
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

420
    let mut results: Vec<(String, i32, Vec<(String, String, i32, String, Vec<String>)>)> = Vec::new();
421
422

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

Daniel Brüning's avatar
Daniel Brüning committed
425
        //TODO: use user
426
        for (user, userdata) in groupdata {
427
428
            let mut userresults: Vec<String> = Vec::new();

429
430
431
            userresults.push(String::new());
            let mut summe = 0;

432
            for grade in userdata {
433
434
435
436
                if let Some(g) = grade.grade {
                    userresults.push(format!("{}", g));
                    summe += g;
                } else {
Robert Czechowski's avatar
Robert Czechowski committed
437
                    userresults.push("–".to_string());
438
                }
439
440
            }

441
442
            userresults[0] = format!("{}", summe);

443
444
            groupresults.push((user.firstname.unwrap_or_else(|| "–".to_string()),
                               user.lastname.unwrap_or_else(|| "–".to_string()),
445
                               user.id,
446
                               grade_to_string(user.grade),
447
                               userresults))
448
        }
449

Robert Czechowski's avatar
Robert Czechowski committed
450
        results.push((group.name.to_string(), group.id.unwrap_or(0), groupresults));
451
452
453
454
    }

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
455
456

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

459
    data.insert("contest".to_string(), to_json(&ci));
460
    data.insert("contestname".to_string(), to_json(&c.name));
461

462
463
464
    Ok(("contestresults".to_owned(), data))
}

465
pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str)
Robert Czechowski's avatar
Robert Czechowski committed
466
                                         -> MedalResult<()> {
467
    // TODO: Is _or_new the right semantic? We need a CSRF token anyway …
468
    let session = conn.get_session_or_new(&session_token);
469
    let contest = conn.get_contest_by_id(contest_id);
470

471
    // Check logged in or open contest
472
    if contest.duration != 0 && !session.is_logged_in() {
473
        return Err(MedalError::AccessDenied);
474
    }
475

476
    // Check CSRF token
477
478
479
    if session.is_logged_in() && session.csrf_token != csrf_token {
        return Err(MedalError::CsrfCheckFailed);
    }
480

481
482
483
484
485
486
487
    // Check other constraints
    let constraints = check_contest_constraints(&session, &contest);

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

488
    // Start contest
489
    match conn.new_participation(&session_token, contest_id) {
Robert Czechowski's avatar
Robert Czechowski committed
490
        Ok(_) => Ok(()),
491
        _ => Err(MedalError::AccessDenied), // Contest already started TODO: Maybe redirect to page with hint
Robert Czechowski's avatar
Robert Czechowski committed
492
    }
Robert Czechowski's avatar
Robert Czechowski committed
493
494
}

495
pub fn login<T: MedalConnection>(conn: &T, login_data: (String, String),
496
                                 oauth_infos: (Option<String>, Option<Vec<OauthProvider>>))
497
498
                                 -> Result<String, MedalValue>
{
Robert Czechowski's avatar
Robert Czechowski committed
499
500
    let (username, password) = login_data;

501
    match conn.login(None, &username, &password) {
Robert Czechowski's avatar
Robert Czechowski committed
502
        Ok(session_token) => Ok(session_token),
Robert Czechowski's avatar
Robert Czechowski committed
503
504
        Err(()) => {
            let mut data = json_val::Map::new();
505
            data.insert("reason".to_string(), to_json(&"Login fehlgeschlagen. Bitte erneut versuchen.".to_string()));
Robert Czechowski's avatar
Robert Czechowski committed
506
            data.insert("username".to_string(), to_json(&username));
507
508
            data.insert("parent".to_string(), to_json(&"base"));

509
            fill_oauth_data(oauth_infos, &mut data);
510

511
            Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
512
513
514
515
        }
    }
}

Robert Czechowski's avatar
Robert Czechowski committed
516
pub fn login_with_code<T: MedalConnection>(
517
    conn: &T, code: &str, oauth_infos: (Option<String>, Option<Vec<OauthProvider>>))
Robert Czechowski's avatar
Robert Czechowski committed
518
    -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
519
    match conn.login_with_code(None, &code) {
Robert Czechowski's avatar
Robert Czechowski committed
520
521
522
523
524
525
526
        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));
527
528
                data.insert("parent".to_string(), to_json(&"base"));

529
                fill_oauth_data(oauth_infos, &mut data);
530

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

537
pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
538
    session_token.map(|token| conn.logout(&token));
Robert Czechowski's avatar
Robert Czechowski committed
539
540
}

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

545
    match match subtask {
Robert Czechowski's avatar
Robert Czechowski committed
546
547
548
              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
549
        Some(submission) => Ok(submission.value),
Robert Czechowski's avatar
Robert Czechowski committed
550
        None => Ok("{}".to_string()),
Robert Czechowski's avatar
Robert Czechowski committed
551
552
553
    }
}

554
555
pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, csrf_token: &str,
                                           data: String, grade: i32, subtask: Option<String>)
Robert Czechowski's avatar
Robert Czechowski committed
556
557
                                           -> MedalResult<String>
{
558
    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
559
560

    if session.csrf_token != csrf_token {
561
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
562
563
    }

564
565
566
567
568
569
570
571
572
573
574
575
    let (_, _, c) = conn.get_task_by_id_complete(task_id);

    match conn.get_participation(&session_token, c.id.expect("Value from database")) {
        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;
576
            if c.duration > 0 && left_secs < -10 {
577
578
579
                return Err(MedalError::AccessDenied);
                // Contest over
                // TODO: Nicer message!
580
581
582
583
            }
        }
    }

Robert Czechowski's avatar
Robert Czechowski committed
584
585
586
    let submission = Submission { id: None,
                                  session_user: session.id,
                                  task: task_id,
Robert Czechowski's avatar
Robert Czechowski committed
587
                                  grade,
Robert Czechowski's avatar
Robert Czechowski committed
588
589
590
591
592
593
                                  validated: false,
                                  nonvalidated_grade: grade,
                                  needs_validation: true,
                                  subtask_identifier: subtask,
                                  value: data,
                                  date: time::get_time() };
594

Robert Czechowski's avatar
Robert Czechowski committed
595
596
597
598
599
    conn.submit_submission(submission);

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

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

603
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
604
605
    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?
606

607
608
609
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
610

611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
    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);
        }
    }
626

627
    match conn.get_participation(&session_token, c.id.expect("Value from database")) {
628
        None => Err(MedalError::AccessDenied),
629
630
631
632
633
634
        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
635

636
            let mut data = json_val::Map::new();
637
            data.insert("participation_start_date".to_string(), to_json(&format!("{}", passed_secs)));
638
639
640
            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
641

642
            let left_secs = i64::from(c.duration) * 60 - passed_secs;
643
            if c.duration > 0 && left_secs < 0 {
644
                Err(MedalError::AccessDenied)
645
646
647
648
649
            // 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
650
651
                data.insert("time_left".to_string(), to_json(&format!("{}:{:02}", hour, min)));
                data.insert("time_left_sec".to_string(), to_json(&format!(":{:02}", sec)));
652
653

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

655
                data.insert("contestname".to_string(), to_json(&c.name));
656
657
                data.insert("name".to_string(), to_json(&tg.name));
                data.insert("taskid".to_string(), to_json(&task_id));
658
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
659
660
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
661
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
662

Robert Czechowski's avatar
Robert Czechowski committed
663
664
665
666
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

667
668
669
670
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
671
}
Robert Czechowski's avatar
Robert Czechowski committed
672

Robert Czechowski's avatar
Robert Czechowski committed
673
674
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
675
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
676
677
678
679
680
    pub name: String,
    pub tag: String,
    pub code: String,
}

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

Robert Czechowski's avatar
Robert Czechowski committed
684
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
685
686

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

Robert Czechowski's avatar
Robert Czechowski committed
689
690
691
692
693
694
695
696
    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
697
    data.insert("group".to_string(), to_json(&v));
698
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
699

700
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
701
702
}

Robert Czechowski's avatar
Robert Czechowski committed
703
704
#[derive(Serialize, Deserialize)]
pub struct MemberInfo {
705
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
706
707
    pub firstname: String,
    pub lastname: String,
708
    pub grade: String,
Robert Czechowski's avatar
Robert Czechowski committed
709
710
711
    pub logincode: String,
}

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

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

Robert Czechowski's avatar
Robert Czechowski committed
719
    if group.admin != session.id {
720
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
721
722
    }

Robert Czechowski's avatar
Robert Czechowski committed
723
724
725
726
727
728
729
730
731
732
733
    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()),
734
                                   grade: grade_to_string(m.grade),
Robert Czechowski's avatar
Robert Czechowski committed
735
736
                                   logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()) })
             .collect();
Robert Czechowski's avatar
Robert Czechowski committed
737
738
739

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
740
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
741
742
743

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

745
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
746
747
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
748

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

    if session.csrf_token != csrf_token {
754
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
755
756
    }

Robert Czechowski's avatar
Robert Czechowski committed
757
    let groupcode = helpers::make_group_code();
758
    // TODO: check for collisions
759

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

Robert Czechowski's avatar
Robert Czechowski committed
762
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
763
764
765

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

767
pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
768
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
769
770
771
772
773
774
775

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

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

776
// TODO: Should creating the users and groups happen in a batch operation to speed things up?
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
777
778
pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
                                         -> MedalResult<()> {
779
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
780
781
782
783
784

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

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

787
788
789
790
791
    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
792
793
794
795
796
797
798
    let mut group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: "".to_string(),
                            admin: session.id,
                            members: Vec::new() };

799
800
    for line in v {
        if name != line[0] {
801
            if name != "" {
802
                conn.create_group_with_users(group);
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
803
            }
804
805
806
807
            name = line[0].clone();
            group_code = helpers::make_group_code();
            // TODO: check for collisions

Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
808
809
810
811
812
813
            group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: name.clone(),
                            admin: session.id,
                            members: Vec::new() };
814
815
816
817
818
819
820
821
822
823
824
        }

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

825
826
827
    Ok(())
}

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

834
    let data = json_val::Map::new();
835
836
837

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

839
pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
Robert Czechowski's avatar
Robert Czechowski committed
840
841
842
                                        query_string: Option<String>)
                                        -> MedalValueResult
{
843
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
844
845

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

848
849
    match user_id {
        None => {
850
851
852
853
854
            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));
855
            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
856
857
858
859
860
            if let Some(sex) = session.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
861

862
            data.insert("profile_logincode".to_string(), to_json(&session.logincode));
863
            if session.password.is_some() {
864
                data.insert("profile_username".to_string(), to_json(&session.username));
865
866
            }
            if session.managed_by.is_none() {
867
                data.insert("profile_not_in_group".into(), to_json(&true));
868
            }
869
870
871
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
872
873
            data.insert("ownprofile".into(), to_json(&true));

874
            if let Some(query) = query_string {
875
                if query.starts_with("status=") {
876
                    let status: &str = &query[7..];
877
                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch", "firstlogin"].contains(&status) {
878
879
                        data.insert((status).to_string(), to_json(&true));
                    }
880
881
                }
            }
882

883
884
            let now = time::get_time();

885
            // TODO: Needs to be filtered
886
887
888
889
890
891
892
893
894
895
896
897
            let participations: Vec<(i32, String, bool, bool)> =
                conn.get_all_participations_complete(session.id)
                    .into_iter()
                    .rev()
                    .map(|(participation, contest)| {
                        let passed_secs = now.sec - participation.start.sec;
                        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
                        let is_time_left = contest.duration == 0 || left_secs >= 0;
                        let has_timelimit = contest.duration != 0;
                        (contest.id.unwrap(), contest.name, has_timelimit, is_time_left)
                    })
                    .collect();
898
            data.insert("participations".into(), to_json(&participations));
Robert Czechowski's avatar
Robert Czechowski committed
899
        }
900
901
902
903
904
905
906
907
        Some(user_id) => {
            // TODO: Add test to check if this access restriction works
            let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
            let group = opt_group.ok_or(MedalError::AccessDenied)?;
            if group.admin != session.id {
                return Err(MedalError::AccessDenied);
            }

908
909
910
911
912
            data.insert("profile_firstname".to_string(), to_json(&user.firstname));
            data.insert("profile_lastname".to_string(), to_json(&user.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));
913
            data.insert(format!("sel{}", user.grade), to_json(&"selected"));
914
915
916
917
918
            if let Some(sex) = user.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
919

920
            data.insert("profile_logincode".to_string(), to_json(&user.logincode));
921
            if user.username.is_some() {
922
                data.insert("profile_username".to_string(), to_json(&user.username));
923
924
            }
            if user.managed_by.is_none() {
925
                data.insert("profile_not_in_group".into(), to_json(&true));
926
            }
927
928
929
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
930
            data.insert("ownprofile".into(), to_json(&false));
931

932
933
934
935
936
937
938
939
            if let Some(query) = query_string {
                if query.starts_with("status=") {
                    let status: &str = &query[7..];
                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
                        data.insert((status).to_string(), to_json(&true));
                    }
                }
            }
940
941
        }
    }
942

943
    Ok(("profile".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
944
945
}

946
#[derive(Debug, PartialEq)]
947
pub enum ProfileStatus {
Daniel Brüning's avatar
Daniel Brüning committed