core.rs 57.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;

17
use db_conn::{MedalConnection, SignupResult};
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
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
pub fn signup<T: MedalConnection>(conn: &T, session_token:Option<String>, signup_data: (String, String, String)) -> MedalResult<SignupResult> {
    let (username, email, password) = signup_data;

    if username == "" || email == "" || password == "" {
        return Ok(SignupResult::EmptyFields)
    }

    let salt = helpers::make_salt();
    let hash = helpers::hash_password(&password, &salt)?;

    let result = conn.signup(&session_token.unwrap(), &username, &email, hash, &salt);
    Ok(result)
}

pub fn signupdata (query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
    let mut data = json_val::Map::new();
    if let Some(query) = query_string {
        if query.starts_with("status=") {
            let status: &str = &query[7..];
            if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) {
                data.insert((status).to_string(), to_json(&true));
            }
        }
    }
    data
}

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

567
    match match subtask {
Robert Czechowski's avatar
Robert Czechowski committed
568
569
570
              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
571
        Some(submission) => Ok(submission.value),
Robert Czechowski's avatar
Robert Czechowski committed
572
        None => Ok("{}".to_string()),
Robert Czechowski's avatar
Robert Czechowski committed
573
574
575
    }
}

576
577
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
578
579
                                           -> MedalResult<String>
{
580
    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
581
582

    if session.csrf_token != csrf_token {
583
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
584
585
    }

586
587
588
589
590
591
592
593
594
595
596
597
    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;
598
            if c.duration > 0 && left_secs < -10 {
599
600
601
                return Err(MedalError::AccessDenied);
                // Contest over
                // TODO: Nicer message!
602
603
604
605
            }
        }
    }

Robert Czechowski's avatar
Robert Czechowski committed
606
607
608
    let submission = Submission { id: None,
                                  session_user: session.id,
                                  task: task_id,
Robert Czechowski's avatar
Robert Czechowski committed
609
                                  grade,
Robert Czechowski's avatar
Robert Czechowski committed
610
611
612
613
614
615
                                  validated: false,
                                  nonvalidated_grade: grade,
                                  needs_validation: true,
                                  subtask_identifier: subtask,
                                  value: data,
                                  date: time::get_time() };
616

Robert Czechowski's avatar
Robert Czechowski committed
617
618
619
620
621
    conn.submit_submission(submission);

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

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

625
    let (t, tg, c) = conn.get_task_by_id_complete(task_id);
626
627
    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?
628

629
630
631
    let mut prevtaskgroup: Option<Taskgroup> = None;
    let mut nexttaskgroup: Option<Taskgroup> = None;
    let mut current_found = false;
632

633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
    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);
        }
    }
648

649
    match conn.get_participation(&session_token, c.id.expect("Value from database")) {
650
        None => Err(MedalError::AccessDenied),
651
652
653
654
655
656
        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
657

658
            let mut data = json_val::Map::new();
659
            data.insert("participation_start_date".to_string(), to_json(&format!("{}", passed_secs)));
660
661
662
            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
663

664
            let left_secs = i64::from(c.duration) * 60 - passed_secs;
665
            if c.duration > 0 && left_secs < 0 {
666
                Err(MedalError::AccessDenied)
667
668
669
670
671
            // 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
672
673
                data.insert("time_left".to_string(), to_json(&format!("{}:{:02}", hour, min)));
                data.insert("time_left_sec".to_string(), to_json(&format!(":{:02}", sec)));
674
675

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

677
                data.insert("contestname".to_string(), to_json(&c.name));
678
679
                data.insert("name".to_string(), to_json(&tg.name));
                data.insert("taskid".to_string(), to_json(&task_id));
680
                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
681
682
                data.insert("taskpath".to_string(), to_json(&taskpath));
                data.insert("contestid".to_string(), to_json(&c.id));
Robert Czechowski's avatar
Robert Czechowski committed
683
                data.insert("seconds_left".to_string(), to_json(&left_secs));
Robert Czechowski's avatar
Robert Czechowski committed
684

Robert Czechowski's avatar
Robert Czechowski committed
685
686
687
688
                if c.duration > 0 {
                    data.insert("duration".to_string(), to_json(&true));
                }

689
690
691
692
                Ok(("task".to_owned(), data))
            }
        }
    }
Robert Czechowski's avatar
Robert Czechowski committed
693
}
Robert Czechowski's avatar
Robert Czechowski committed
694

Robert Czechowski's avatar
Robert Czechowski committed
695
696
#[derive(Serialize, Deserialize)]
pub struct GroupInfo {
697
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
698
699
700
701
702
    pub name: String,
    pub tag: String,
    pub code: String,
}

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

Robert Czechowski's avatar
Robert Czechowski committed
706
    //    let groupvec = conn.get_group(session_token);
Robert Czechowski's avatar
Robert Czechowski committed
707
708

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

Robert Czechowski's avatar
Robert Czechowski committed
711
712
713
714
715
716
717
718
    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
719
    data.insert("group".to_string(), to_json(&v));
720
    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
721

722
    Ok(("groups".to_string(), data))
Robert Czechowski's avatar
Robert Czechowski committed
723
724
}

Robert Czechowski's avatar
Robert Czechowski committed
725
726
#[derive(Serialize, Deserialize)]
pub struct MemberInfo {
727
    pub id: i32,
Robert Czechowski's avatar
Robert Czechowski committed
728
729
    pub firstname: String,
    pub lastname: String,
730
    pub grade: String,
Robert Czechowski's avatar
Robert Czechowski committed
731
732
733
    pub logincode: String,
}

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

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

Robert Czechowski's avatar
Robert Czechowski committed
741
    if group.admin != session.id {
742
        return Err(MedalError::AccessDenied);
Robert Czechowski's avatar
Robert Czechowski committed
743
744
    }

Robert Czechowski's avatar
Robert Czechowski committed
745
746
747
748
749
750
751
752
753
754
755
    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()),
756
                                   grade: grade_to_string(m.grade),
Robert Czechowski's avatar
Robert Czechowski committed
757
758
                                   logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()) })
             .collect();
Robert Czechowski's avatar
Robert Czechowski committed
759
760
761

    data.insert("group".to_string(), to_json(&gi));
    data.insert("member".to_string(), to_json(&v));
762
    data.insert("groupname".to_string(), to_json(&gi.name));
Robert Czechowski's avatar
Robert Czechowski committed
763
764
765

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

767
pub fn modify_group<T: MedalConnection>(_conn: &T, _group_id: i32, _session_token: &str) -> MedalResult<()> {
Robert Czechowski's avatar
Robert Czechowski committed
768
769
    unimplemented!()
}
Robert Czechowski's avatar
Robert Czechowski committed
770

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

    if session.csrf_token != csrf_token {
776
        return Err(MedalError::CsrfCheckFailed);
Robert Czechowski's avatar
Robert Czechowski committed
777
778
    }

Robert Czechowski's avatar
Robert Czechowski committed
779
    let groupcode = helpers::make_group_code();
780
    // TODO: check for collisions
781

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

Robert Czechowski's avatar
Robert Czechowski committed
784
    conn.add_group(&mut group);
Robert Czechowski's avatar
Robert Czechowski committed
785
786
787

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

789
pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
790
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
791
792
793
794
795
796
797

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

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

798
// TODO: Should creating the users and groups happen in a batch operation to speed things up?
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
799
800
pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
                                         -> MedalResult<()> {
801
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
802
803
804
805
806

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

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

809
810
811
812
813
    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
814
815
816
817
818
819
820
    let mut group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: "".to_string(),
                            admin: session.id,
                            members: Vec::new() };

821
822
    for line in v {
        if name != line[0] {
823
            if name != "" {
824
                conn.create_group_with_users(group);
Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
825
            }
826
827
828
829
            name = line[0].clone();
            group_code = helpers::make_group_code();
            // TODO: check for collisions

Robert Czechowski's avatar
rustfmt    
Robert Czechowski committed
830
831
832
833
834
835
            group = Group { id: None,
                            name: name.clone(),
                            groupcode: group_code,
                            tag: name.clone(),
                            admin: session.id,
                            members: Vec::new() };
836
837
838
839
840
841
842
843
844
845
846
        }

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

847
848
849
    Ok(())
}

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

856
    let data = json_val::Map::new();
857
858
859

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

861
pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
Robert Czechowski's avatar
Robert Czechowski committed
862
863
864
                                        query_string: Option<String>)
                                        -> MedalValueResult
{
865
    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
Robert Czechowski's avatar
Robert Czechowski committed
866
867

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

870
871
    match user_id {
        None => {
872
873
874
875
876
            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));
877
            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
878
879
880
881
882
            if let Some(sex) = session.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
883

884
            data.insert("profile_logincode".to_string(), to_json(&session.logincode));
885
            if session.password.is_some() {
886
                data.insert("profile_username".to_string(), to_json(&session.username));
887
888
            }
            if session.managed_by.is_none() {
889
                data.insert("profile_not_in_group".into(), to_json(&true));
890
            }
891
892
893
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
894
895
            data.insert("ownprofile".into(), to_json(&true));

896
            if let Some(query) = query_string {
897
                if query.starts_with("status=") {
898
                    let status: &str = &query[7..];
899
                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch", "firstlogin", "SignedUp"].contains(&status) {
900
901
                        data.insert((status).to_string(), to_json(&true));
                    }
902
903
                }
            }
904

905
906
            let now = time::get_time();

907
            // TODO: Needs to be filtered
908
909
910
911
912
913
914
915
            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
916
        }
917
        // Case user_id: teacher modifing a students profile
918
919
920
921
922
923
924
925
        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);
            }

926
927
928
929
930
            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));
931
            data.insert(format!("sel{}", user.grade), to_json(&"selected"));
932
933
934
935
936
            if let Some(sex) = user.sex {
                data.insert(format!("sex_{}", sex), to_json(&"selected"));
            } else {
                data.insert("sex_None".to_string(), to_json(&"selected"));
            }
937

938
            data.insert("profile_logincode".to_string(), to_json(&user.logincode));
939
            if user.username.is_some() {
940
                data.insert("profile_username".to_string(), to_json(&user.username));
941
942
            }
            if user.managed_by.is_none() {
943
                data.insert("profile_not_in_group".into(), to_json(&true));
944
            }
945
946
947
            if session.oauth_provider != Some("pms".to_string()) {
                data.insert("profile_not_pms".into(), to_json(&true));
            }
948
            data.insert("ownprofile".into(), to_json(&false));
949

950
951
952
953
954
955
956
957
            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));
                    }
                }
            }
958
959
        }
    }
960

961
    Ok(("profile"