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

15
16
use time;

Robert Czechowski's avatar
Robert Czechowski committed
17
use db_conn::MedalConnection;
18
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
    fill_oauth_data(oauth_infos, &mut data);
Robert Czechowski's avatar
Robert Czechowski committed
303

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

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

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

313
314
315
316
    // 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) {
317
318
319
320
321
322
    if opt_part.is_none()
       && contest.duration == 0
       && constraints.contest_running
       && constraints.grade_matching
       && contest.requires_login != Some(true)
    {
323
324
325
326
        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() });
    }

327
    let now = time::get_time();
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
    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));
351
352
    }

353
354
355
    if let Some(participation) = opt_part {
        let mut totalgrade = 0;
        let mut max_totalgrade = 0;
356

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

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

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

382
        data.insert("is_started".to_string(), to_json(&true));
383
        data.insert("participation_start_date".to_string(), to_json(&passed_secs));
384
385
        data.insert("total_points".to_string(), to_json(&totalgrade));
        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
386
387
388
        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));
389
390
391
392
393
394
        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
395
    }
396

397
398
399
400
401
402
    // 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));
403
404
    }

405
    Ok(("contest".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
406
407
}

408
pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
409
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
410
411
    let mut data = json_val::Map::new();
    fill_user_data(&session, &mut data);
412

413
414
    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);

415
    let mut results: Vec<(String, i32, Vec<(String, String, i32, String, Vec<String>)>)> = Vec::new();
416
417

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

Daniel Brüning's avatar
Daniel Brüning committed
420
        //TODO: use user
421
        for (user, userdata) in groupdata {
422
423
            let mut userresults: Vec<String> = Vec::new();

424
425
426
            userresults.push(String::new());
            let mut summe = 0;

427
            for grade in userdata {
428
429
430
431
                if let Some(g) = grade.grade {
                    userresults.push(format!("{}", g));
                    summe += g;
                } else {
Robert Czechowski's avatar
Robert Czechowski committed
432
                    userresults.push("–".to_string());
433
                }
434
435
            }

436
437
            userresults[0] = format!("{}", summe);

438
439
            groupresults.push((user.firstname.unwrap_or_else(|| "–".to_string()),
                               user.lastname.unwrap_or_else(|| "–".to_string()),
440
                               user.id,
441
                               grade_to_string(user.grade),
442
                               userresults))
443
        }
444

Robert Czechowski's avatar
Robert Czechowski committed
445
        results.push((group.name.to_string(), group.id.unwrap_or(0), groupresults));
446
447
448
449
    }

    data.insert("taskname".to_string(), to_json(&tasknames));
    data.insert("result".to_string(), to_json(&results));
450
451

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

454
    data.insert("contest".to_string(), to_json(&ci));
455
    data.insert("contestname".to_string(), to_json(&c.name));
456

457
458
459
    Ok(("contestresults".to_owned(), data))
}

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

466
    // Check logged in or open contest
467
    if contest.duration != 0 && !session.is_logged_in() {
468
        return Err(MedalError::AccessDenied);
469
    }
470

471
    // Check CSRF token
472
473
474
    if session.is_logged_in() && session.csrf_token != csrf_token {
        return Err(MedalError::CsrfCheckFailed);
    }
475

476
477
478
479
480
481
482
    // Check other constraints
    let constraints = check_contest_constraints(&session, &contest);

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

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

490
pub fn login<T: MedalConnection>(conn: &T, login_data: (String, String),
491
                                 oauth_infos: (Option<String>, Option<Vec<OauthProvider>>))
492
493
                                 -> Result<String, MedalValue>
{
Robert Czechowski's avatar
Robert Czechowski committed
494
495
    let (username, password) = login_data;

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

504
            fill_oauth_data(oauth_infos, &mut data);
505

506
            Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
507
508
509
510
        }
    }
}

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

524
                fill_oauth_data(oauth_infos, &mut data);
525

Robert Czechowski's avatar
Robert Czechowski committed
526
                Err(("login".to_owned(), data))
Robert Czechowski's avatar
Robert Czechowski committed
527
            }
Robert Czechowski's avatar
Robert Czechowski committed
528
        },
Robert Czechowski's avatar
Robert Czechowski committed
529
530
531
    }
}

532
pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
533
    session_token.map(|token| conn.logout(&token));
Robert Czechowski's avatar
Robert Czechowski committed
534
535
}

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

540
    match match subtask {
Robert Czechowski's avatar
Robert Czechowski committed
541
542
543
              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
544
        Some(submission) => Ok(submission.value),
Robert Czechowski's avatar
Robert Czechowski committed
545
        None => Ok("{}".to_string()),
Robert Czechowski's avatar
Robert Czechowski committed
546
547
548
    }
}

549
550
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
551
552
                                           -> MedalResult<String>
{
553
    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
554
555

    if session.csrf_token != csrf_token {
556
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
557
558
    }

559
560
561
562
563
564
565
566
567
568
569
570
    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;
571
            if c.duration > 0 && left_secs < -10 {
572
573
574
                return Err(MedalError::AccessDenied);
                // Contest over
                // TODO: Nicer message!
575
576
577
578
            }
        }
    }

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

Robert Czechowski's avatar
Robert Czechowski committed
590
591
592
593
594
    conn.submit_submission(submission);

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

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

598
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
599
600
    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?
601

602
603
604
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
605

606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
    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);
        }
    }
621

622
    match conn.get_participation(&session_token, c.id.expect("Value from database")) {
623
        None => Err(MedalError::AccessDenied),
624
625
626
627
628
629
        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
630

631
            let mut data = json_val::Map::new();
632
            data.insert("participation_start_date".to_string(), to_json(&format!("{}", passed_secs)));
633
634
635
            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
636

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

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

650
                data.insert("contestname".to_string(), to_json(&c.name));
651
652
                data.insert("name".to_string(), to_json(&tg.name));
                data.insert("taskid".to_string(), to_json(&task_id));
653
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
654
655
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
656
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
657

Robert Czechowski's avatar
Robert Czechowski committed
658
659
660
661
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

662
663
664
665
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
666
}
Robert Czechowski's avatar
Robert Czechowski committed
667

Robert Czechowski's avatar
Robert Czechowski committed
668
669
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
670
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
671
672
673
674
675
    pub name: String,
    pub tag: String,
    pub code: String,
}

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

Robert Czechowski's avatar
Robert Czechowski committed
679
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
680
681

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

Robert Czechowski's avatar
Robert Czechowski committed
684
685
686
687
688
689
690
691
    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
692
    data.insert("group".to_string(), to_json(&v));
693
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
694

695
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
696
697
}

Robert Czechowski's avatar
Robert Czechowski committed
698
699
#[derive(Serialize, Deserialize)]
pub struct MemberInfo {
700
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
701
702
    pub firstname: String,
    pub lastname: String,
703
    pub grade: String,
Robert Czechowski's avatar
Robert Czechowski committed
704
705
706
    pub logincode: String,
}

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

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

Robert Czechowski's avatar
Robert Czechowski committed
714
    if group.admin != session.id {
715
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
716
717
    }

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

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
735
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
736
737
738

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

740
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
741
742
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
743

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

    if session.csrf_token != csrf_token {
749
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
750
751
    }

Robert Czechowski's avatar
Robert Czechowski committed
752
    let groupcode = helpers::make_group_code();
753
    // TODO: check for collisions
754

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

Robert Czechowski's avatar
Robert Czechowski committed
757
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
758
759
760

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

762
pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
763
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
764
765
766
767
768
769
770

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

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

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

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

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

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

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

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

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

820
821
822
    Ok(())
}

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

829
    let data = json_val::Map::new();
830
831
832

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

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

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

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

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

869
            if let Some(query) = query_string {
870
                if query.starts_with("status=") {
871
                    let status: &str = &query[7..];
872
                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch", "firstlogin"].contains(&status) {
873
874
                        data.insert((status).to_string(), to_json(&true));
                    }
875
876
                }
            }
877

878
879
            let now = time::get_time();

880
            // TODO: Needs to be filtered
881
882
883
884
885
886
887
888
            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();
            data.insert("participations".into(), to_json(&participations));
Robert Czechowski's avatar
Robert Czechowski committed
889
        }
890
891
892
893
894
895
896
897
        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);
            }

898
899
900
901
902
            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));
903
            data.insert(format!("sel{}", user.grade), to_json(&"selected"));
904
905
906
907
908
            if let Some(sex) = user.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
909

910
            data.insert("profile_logincode".to_string(), to_json(&user.logincode));
911
            if user.username.is_some() {
912
                data.insert("profile_username".to_string(), to_json(&user.username));
913
914
            }
            if user.managed_by.is_none() {
915
                data.insert("profile_not_in_group".into(), to_json(&true));
916
            }
917
918
919
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
920
            data.insert("ownprofile".into(), to_json(&false));
921

922
923
924
925
926
927
928
929
            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));
                    }
                }
            }
930
931
        }
    }
932

933
    Ok(("profile".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
934
935
}

936
#[derive(Debug, PartialEq)]
937
pub enum ProfileStatus {
Daniel Brüning's avatar
Daniel Brüning committed
938
939
940
941
    NothingChanged,
    DataChanged,
    PasswordChanged,
    PasswordMissmatch,
942
943
}
impl std::convert::Into<String> for ProfileStatus {
Robert Czechowski's avatar
Robert Czechowski committed
944
    fn into(self) -> String { format!("{:?}", self) }
945
946
}