1use time;
16
17use config::OauthProvider;
18use db_conn::MedalConnection;
19#[cfg(feature = "signup")]
20use db_conn::SignupResult;
21use db_objects::OptionSession;
22use db_objects::SessionUser;
23use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
24use helpers;
25use webauthn_rs::prelude as webauthn;
26use webfw_iron::{json_val, to_json};
27
28#[derive(Serialize, Deserialize)]
29pub struct SubTaskInfo {
30 pub id: i32,
31 pub linktext: String,
32 pub active: bool,
33 pub greyout: bool,
34}
35
36#[derive(Serialize, Deserialize)]
37pub struct TaskInfo {
38 pub name: String,
39 pub subtasks: Vec<SubTaskInfo>,
40}
41
42#[derive(Clone, Serialize, Deserialize)]
43pub struct ContestInfo {
44 pub id: i32,
45 pub name: String,
46 pub duration: i32,
47 pub public: bool,
48 pub requires_login: bool,
49 pub image: Option<String>,
50 pub language: Option<String>,
51 pub category: Option<String>,
52 pub team_participation: bool,
53 pub tags: Vec<String>,
54}
55
56#[derive(Clone, Debug)]
57pub enum MedalError {
58 NotLoggedIn,
59 AccessDenied,
60 CsrfCheckFailed,
61 SessionTimeout,
62 DatabaseError,
63 ConfigurationError,
64 DatabaseConnectionError,
65 PasswordHashingError,
66 UnmatchedPasswords,
67 NotFound,
68 AccountIncomplete,
69 UnknownId,
70 OauthError(String),
71 WebauthnError,
72 ErrorWithJson(json_val::Map<String, json_val::Value>),
73}
74
75pub struct LoginInfo {
76 pub password_login: bool,
77 pub self_url: Option<String>,
78 pub oauth_providers: Option<Vec<OauthProvider>>,
79}
80
81type MedalValue = (String, json_val::Map<String, json_val::Value>);
82type MedalResult<T> = Result<T, MedalError>;
83type MedalValueResult = MedalResult<MedalValue>;
84
85type JsonValue = json_val::Map<String, json_val::Value>;
86type JsonValueResult = MedalResult<JsonValue>;
87
88fn fill_user_data_prefix(session: &SessionUser, data: &mut json_val::Map<String, serde_json::Value>, prefix: &str) {
89 data.insert(prefix.to_string() + "username", to_json(&session.username));
90 data.insert(prefix.to_string() + "firstname", to_json(&session.firstname));
91 data.insert(prefix.to_string() + "lastname", to_json(&session.lastname));
92 data.insert(prefix.to_string() + "teacher", to_json(&session.is_teacher));
93 data.insert(prefix.to_string() + "is_teacher", to_json(&session.is_teacher));
94 data.insert(prefix.to_string() + "teacher_schoolname", to_json(&session.school_name));
95 data.insert(prefix.to_string() + "admin", to_json(&session.is_admin));
96 data.insert(prefix.to_string() + "is_admin", to_json(&session.is_admin));
97 data.insert(prefix.to_string() + "logged_in", to_json(&session.is_logged_in()));
98 data.insert(prefix.to_string() + "csrf_token", to_json(&session.csrf_token));
99 data.insert(prefix.to_string() + "sex",
100 to_json(&(match session.sex {
101 Some(0) | None => "/",
102 Some(1) => "m",
103 Some(2) => "w",
104 Some(3) => "d",
105 Some(4) => "…",
106 _ => "?",
107 })));
108}
109
110fn fill_user_data(session: &SessionUser, data: &mut json_val::Map<String, serde_json::Value>) {
111 fill_user_data_prefix(session, data, "");
112
113 data.insert("parent".to_string(), to_json(&"base"));
114 data.insert("medal_version".to_string(), to_json(&env!("GIT_VERSION")));
115}
116
117fn fill_oauth_data(login_info: LoginInfo, data: &mut json_val::Map<String, serde_json::Value>) {
118 let mut oauth_links: Vec<(String, String, String)> = Vec::new();
119 if let Some(oauth_providers) = login_info.oauth_providers {
120 for oauth_provider in oauth_providers {
121 oauth_links.push((oauth_provider.provider_id.to_owned(),
122 oauth_provider.login_link_text.to_owned(),
123 oauth_provider.url.to_owned()));
124 }
125 }
126
127 data.insert("self_url".to_string(), to_json(&login_info.self_url));
128 data.insert("oauth_links".to_string(), to_json(&oauth_links));
129
130 data.insert("password_login".to_string(), to_json(&login_info.password_login));
131}
132
133fn grade_to_string(grade: i32) -> String {
134 match grade {
135 0 => "Noch kein Schüler".to_string(),
136 n @ 1..=10 => format!("{}", n),
137 11 => "11 (G8)".to_string(),
138 12 => "12 (G8)".to_string(),
139 111 => "11 (G9)".to_string(),
140 112 => "12 (G9)".to_string(),
141 113 => "13 (G9)".to_string(),
142 114 => "Berufsschule".to_string(),
143 255 => "Kein Schüler mehr".to_string(),
144 _ => "?".to_string(),
145 }
146}
147
148fn common_prefix<'a>(a: &'a str, b: &'a str) -> &'a str {
149 let mut ac = a.char_indices();
150 let mut bc = b.char_indices();
151
152 let mut ai;
153 let mut bi;
154 loop {
155 ai = ac.next();
156 bi = bc.next();
157 if ai != bi {
158 break;
159 }
160 if ai.is_none() {
161 break;
162 }
163 }
164 if ai.is_none() {
165 return a;
166 }
167 if bi.is_none() {
168 return b;
169 }
170
171 a.get(..(ai.unwrap().0)).unwrap()
172}
173
174pub fn index<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo) -> MedalValueResult {
175 let mut data = json_val::Map::new();
176
177 if let Some(token) = session_token {
178 if let Some(session) = conn.get_session(&token) {
179 fill_user_data(&session, &mut data);
180
181 if session.logincode.is_some() && session.firstname.is_none() {
182 return Err(MedalError::AccountIncomplete);
183 }
184 }
185 }
186
187 fill_oauth_data(login_info, &mut data);
188
189 let now = time::get_time();
190 let contest_list = conn.get_contest_list();
191 let mut contests_running: Vec<(String, String, (i64, i64))> =
192 contest_list.iter()
193 .filter(|c| c.public)
194 .filter(|c| c.duration != 0 || c.category == Some("contest".to_string()))
195 .filter(|c| !c.requires_login.unwrap_or(false) || c.category.is_some())
196 .filter(|c| c.end.map(|end| now <= end).unwrap_or(false))
197 .filter(|c| c.start.map(|start| now >= start).unwrap_or(true))
198 .map(|c| {
199 (c.name.clone(),
200 if let Some(cat) = c.category.as_ref() {
201 format!("{}/{}", cat, c.id.unwrap_or(0))
202 } else {
203 format!("{}", c.id.unwrap_or(0))
204 },
205 {
206 let t = c.end.unwrap().sec - now.sec;
207 (t / 60 / 60 / 24, t / 60 / 60 % 24)
208 })
209 })
210 .collect();
211
212 let mut i = 0;
213 while i < contests_running.len() {
214 let mut count = 0;
215 while i + 1 < contests_running.len() {
216 if contests_running[i].2 == contests_running[i + 1].2 {
217 contests_running[i].0 = common_prefix(&contests_running[i].0, &contests_running[i + 1].0).to_string();
220 count += 1;
221 contests_running.remove(i + 1);
222 } else {
223 break;
224 }
225 }
226 if count > 0 {
227 contests_running[i].0 = format!("{}… ({}x)", contests_running[i].0, count + 1);
228 contests_running[i].1 = "?filter=current".to_string();
229 }
230 i += 1;
231 }
232
233 if contests_running.len() > 0 {
234 data.insert("current_contests".to_string(), to_json(&contests_running));
235 }
236
237 let mut contests_comming: Vec<(String, String, (i64, i64))> =
238 contest_list.iter()
239 .filter(|c| c.public)
240 .filter(|c| c.duration != 0 || c.category == Some("contest".to_string()))
241 .filter(|c| !c.requires_login.unwrap_or(false) || c.category.is_some())
242 .filter(|c| c.start.map(|start| now <= start).unwrap_or(false))
243 .map(|c| {
244 (c.name.clone(),
245 if let Some(cat) = c.category.as_ref() {
246 format!("{}/{}", cat, c.id.unwrap_or(0))
247 } else {
248 format!("{}", c.id.unwrap_or(0))
249 },
250 {
251 let t = c.start.unwrap().sec - now.sec;
252 (t / 60 / 60 / 24, t / 60 / 60 % 24)
253 })
254 })
255 .collect();
256
257 let mut i = 0;
258 while i < contests_comming.len() {
259 let mut count = 0;
260 while i + 1 < contests_comming.len() {
261 if contests_comming[i].2 == contests_comming[i + 1].2 {
262 contests_comming[i].0 = common_prefix(&contests_comming[i].0, &contests_comming[i + 1].0).to_string();
265 count += 1;
266 contests_comming.remove(i + 1);
267 } else {
268 break;
269 }
270 }
271 if count > 0 {
272 contests_comming[i].0 = format!("{}… ({}x)", contests_comming[i].0, count + 1);
273 contests_comming[i].1 = "?filter=current".to_string();
274 }
275 i += 1;
276 }
277
278 if contests_comming.len() > 0 {
279 data.insert("upcoming_contests".to_string(), to_json(&contests_comming));
280 }
281
282 data.insert("parent".to_string(), to_json(&"base"));
283 data.insert("index".to_string(), to_json(&true));
284 Ok(("index".to_owned(), data))
285}
286
287pub fn show_login<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo)
288 -> (String, json_val::Map<String, json_val::Value>) {
289 let mut data = json_val::Map::new();
290
291 if let Some(token) = session_token {
292 if let Some(session) = conn.get_session(&token) {
293 fill_user_data(&session, &mut data);
294 }
295 }
296
297 fill_oauth_data(login_info, &mut data);
298
299 data.insert("parent".to_string(), to_json(&"base"));
300 ("login".to_owned(), data)
301}
302
303pub fn status<T: MedalConnection>(conn: &T, config_secret: Option<String>, given_secret: Option<String>)
304 -> MedalResult<String> {
305 if config_secret == given_secret {
306 Ok(conn.get_debug_information())
307 } else {
308 Err(MedalError::AccessDenied)
309 }
310}
311
312pub fn debug<T: MedalConnection>(conn: &T, session_token: Option<String>)
313 -> (String, json_val::Map<String, json_val::Value>) {
314 let mut data = json_val::Map::new();
315
316 if let Some(token) = session_token {
317 if let Some(session) = conn.get_session(&token) {
318 data.insert("known_session".to_string(), to_json(&true));
319 data.insert("session_id".to_string(), to_json(&session.id));
320 data.insert("now_timestamp".to_string(), to_json(&time::get_time().sec));
321 if let Some(last_activity) = session.last_activity {
322 data.insert("session_timestamp".to_string(), to_json(&last_activity.sec));
323 data.insert("timediff".to_string(), to_json(&(time::get_time() - last_activity).num_seconds()));
324 }
325 if session.is_alive() {
326 data.insert("alive_session".to_string(), to_json(&true));
327 if session.is_logged_in() {
328 data.insert("logged_in".to_string(), to_json(&true));
329 data.insert("username".to_string(), to_json(&session.username));
330 data.insert("firstname".to_string(), to_json(&session.firstname));
331 data.insert("lastname".to_string(), to_json(&session.lastname));
332 data.insert("teacher".to_string(), to_json(&session.is_teacher));
333 data.insert("oauth_provider".to_string(), to_json(&session.oauth_provider));
334 data.insert("oauth_id".to_string(), to_json(&session.oauth_foreign_id));
335 data.insert("logincode".to_string(), to_json(&session.logincode));
336 data.insert("managed_by".to_string(), to_json(&session.managed_by));
337 }
338 }
339 }
340 data.insert("session".to_string(), to_json(&token));
341 } else {
342 data.insert("session".to_string(), to_json(&"No session token given"));
343 }
344
345 ("debug".to_owned(), data)
346}
347
348pub fn debug_create_session<T: MedalConnection>(conn: &T, session_token: Option<String>) {
349 if let Some(token) = session_token {
350 conn.get_session_or_new(&token).unwrap();
351 }
352}
353
354#[derive(PartialEq, Eq, Debug)]
355pub enum ContestVisibility {
356 All,
357 Open,
358 Current,
359 LoginRequired,
360 StandaloneTask,
361 WithSecret(String),
362}
363
364pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str, category: Option<String>,
365 login_info: LoginInfo, visibility: ContestVisibility, is_results: bool)
366 -> MedalValueResult {
367 let mut data = json_val::Map::new();
368
369 let session = conn.get_session_or_new(&session_token).map_err(|_| MedalError::DatabaseConnectionError)?;
370 fill_user_data(&session, &mut data);
371
372 if session.is_logged_in() {
373 data.insert("can_start".to_string(), to_json(&true));
374 }
375
376 fill_oauth_data(login_info, &mut data);
377
378 let contest_list = if is_results {
379 conn.get_contest_list_with_group_member_participations(session.id)
380 } else {
381 conn.get_contest_list()
382 };
383
384 let now = time::get_time();
385 let v: Vec<ContestInfo> =
386 contest_list.iter()
387 .filter(|c| category.as_ref().map(|cat| c.category.as_ref() == Some(cat)).unwrap_or(true))
388 .filter(|c| c.public || matches!(visibility, ContestVisibility::WithSecret(_)))
389 .filter(|c| {
390 if let ContestVisibility::WithSecret(secret) = &visibility {
391 c.secret.as_ref() == Some(secret)
392 } else {
393 true
394 }
395 })
396 .filter(|c| {
397 (!c.standalone_task.unwrap_or(false))
398 || visibility == ContestVisibility::StandaloneTask
399 || category == Some("standalone_task".to_string())
400 })
401 .filter(|c| c.standalone_task.unwrap_or(false) || visibility != ContestVisibility::StandaloneTask)
402 .filter(|c| {
403 c.end.map(|end| now <= end).unwrap_or(true)
404 || (visibility == ContestVisibility::All && (c.category.is_none() || is_results))
405 })
406 .filter(|c| c.duration == 0 || visibility != ContestVisibility::Open)
407 .filter(|c| c.duration != 0 || visibility != ContestVisibility::Current)
408 .filter(|c| c.requires_login.unwrap_or(false) || visibility != ContestVisibility::LoginRequired)
409 .filter(|c| {
410 !c.requires_login.unwrap_or(false)
411 || visibility == ContestVisibility::LoginRequired
412 || visibility == ContestVisibility::All
413 })
414 .map(|c| ContestInfo { id: c.id.unwrap(),
415 name: c.name.clone(),
416 duration: c.duration,
417 public: c.public,
418 requires_login: c.requires_login.unwrap_or(false),
419 image: c.image.as_ref().map(|i| format!("/{}{}", c.location, i)),
420 language: c.language.clone(),
421 category: c.category.clone(),
422 team_participation: false,
423 tags: c.tags.clone() })
424 .collect();
425
426 if category.is_some() {
427 data.insert("contests".to_string(), to_json(&v));
428 } else {
429 let contests_training: Vec<ContestInfo> =
430 v.clone().into_iter().filter(|c| !c.requires_login).filter(|c| c.duration == 0).collect();
431 let contests_contest: Vec<ContestInfo> =
432 v.clone().into_iter().filter(|c| !c.requires_login).filter(|c| c.duration != 0).collect();
433 let contests_challenge: Vec<ContestInfo> = v.into_iter().filter(|c| c.requires_login).collect();
434
435 data.insert("contests_training".to_string(), to_json(&contests_training));
436 data.insert("contests_contest".to_string(), to_json(&contests_contest));
437 data.insert("contests_challenge".to_string(), to_json(&contests_challenge));
438
439 data.insert("contests_training_header".to_string(), to_json(&"Trainingsaufgaben"));
440 data.insert("contests_contest_header".to_string(), to_json(&"Wettbewerbe"));
441 data.insert("contests_challenge_header".to_string(), to_json(&"Herausforderungen"));
442
443 if visibility == ContestVisibility::StandaloneTask {
444 data.insert("contests_training_header".to_string(), to_json(&"Einzelne Aufgaben ohne Wertung"));
445 }
446 }
447
448 if let ContestVisibility::WithSecret(secret) = visibility {
449 data.insert("secret".to_string(), to_json(&secret));
450 data.insert("has_secret".to_string(), to_json(&true));
451 }
452
453 Ok(("contests".to_owned(), data))
454}
455
456fn generate_subtaskstars(tg: &Taskgroup, grade: &Grade, ast: Option<i32>) -> Vec<SubTaskInfo> {
457 let mut subtaskinfos = Vec::new();
458 let mut not_print_yet = true;
459 for st in &tg.tasks {
460 let mut blackstars: usize = 0;
461 if not_print_yet && st.stars >= grade.grade.unwrap_or(0) {
462 blackstars = grade.grade.unwrap_or(0) as usize;
463 not_print_yet = false;
464 }
465
466 let greyout = not_print_yet && st.stars < grade.grade.unwrap_or(0);
467 let active = ast.is_some() && st.id == ast;
468 let linktext = format!("{}{}",
469 str::repeat("★", blackstars as usize),
470 str::repeat("☆", st.stars as usize - blackstars as usize));
471 let si = SubTaskInfo { id: st.id.unwrap(), linktext, active, greyout };
472
473 subtaskinfos.push(si);
474 }
475 subtaskinfos
476}
477
478#[derive(Serialize, Deserialize)]
479pub struct ContestStartConstraints {
480 pub contest_not_begun: bool,
481 pub contest_over: bool,
482 pub contest_running: bool,
483 pub grade_too_low: bool,
484 pub grade_too_high: bool,
485 pub grade_matching: bool,
486}
487
488fn check_contest_qualification<T: MedalConnection>(conn: &T, session: &SessionUser, contest: &Contest) -> Option<bool> {
489 let required_contests = contest.requires_contest.as_ref()?.split(',');
491
492 for req_contest in required_contests {
493 if conn.has_participation_by_contest_file(session.id, &contest.location, req_contest) {
494 return Some(true);
495 }
496 }
497
498 Some(false)
499}
500
501fn check_contest_constraints(session: &SessionUser, contest: &Contest) -> ContestStartConstraints {
502 let now = time::get_time();
503 let student_grade = session.grade % 100 - if session.grade / 100 == 1 { 1 } else { 0 };
504
505 let contest_not_begun = contest.start.map(|start| now < start).unwrap_or(false);
506 let contest_over = contest.end.map(|end| now > end).unwrap_or(false);
507 let grade_too_low =
508 contest.min_grade.map(|min_grade| student_grade < min_grade && !session.is_teacher).unwrap_or(false);
509 let grade_too_high =
510 contest.max_grade.map(|max_grade| student_grade > max_grade && !session.is_teacher).unwrap_or(false);
511
512 let contest_running = !contest_not_begun && !contest_over;
513 let grade_matching = !grade_too_low && !grade_too_high;
514
515 ContestStartConstraints { contest_not_begun,
516 contest_over,
517 contest_running,
518 grade_too_low,
519 grade_too_high,
520 grade_matching }
521}
522
523#[derive(Serialize, Deserialize)]
524pub struct ContestTimeInfo {
525 pub passed_secs_total: i64,
526 pub left_secs_total: i64,
527 pub left_mins_total: i64,
528 pub left_hour: i64,
529 pub left_min: i64,
530 pub left_sec: i64,
531 pub has_timelimit: bool,
532 pub is_time_left: bool,
533 pub exempt_from_timelimit: bool,
534 pub can_still_compete: bool,
535 pub review_has_timelimit: bool,
536 pub has_future_review: bool,
537 pub has_review_end: bool,
538 pub is_review: bool,
539 pub can_still_compete_or_review: bool,
540
541 pub until_review_start_day: i64,
542 pub until_review_start_hour: i64,
543 pub until_review_start_min: i64,
544
545 pub until_review_end_day: i64,
546 pub until_review_end_hour: i64,
547 pub until_review_end_min: i64,
548}
549
550fn check_contest_time_left(session: &SessionUser, contest: &Contest, participation: &Participation) -> ContestTimeInfo {
551 let now = time::get_time();
552 let passed_secs_total = now.sec - participation.start.sec;
553 if passed_secs_total < 0 {
554 }
556 let left_secs_total = i64::from(contest.duration) * 60 - passed_secs_total;
557
558 let is_time_left = contest.duration == 0 || left_secs_total >= 0;
559 let exempt_from_timelimit = session.is_teacher() || session.is_admin();
560
561 let can_still_compete = is_time_left || exempt_from_timelimit;
562
563 let review_has_timelimit = contest.review_end.is_none() && contest.review_start.is_some();
564 let has_future_review = (contest.review_start.is_some() || contest.review_end.is_some())
565 && contest.review_end.map(|end| end > now).unwrap_or(true);
566 let has_review_end = contest.review_end.is_some();
567 let is_review = !can_still_compete
568 && (contest.review_start.is_some() || contest.review_end.is_some())
569 && contest.review_start.map(|start| now >= start).unwrap_or(true)
570 && contest.review_end.map(|end| now <= end).unwrap_or(true);
571
572 let until_review_start = contest.review_start.map(|start| start.sec - now.sec).unwrap_or(0);
573 let until_review_end = contest.review_end.map(|end| end.sec - now.sec).unwrap_or(0);
574
575 ContestTimeInfo { passed_secs_total,
576 left_secs_total,
577 left_mins_total: left_secs_total / 60,
578 left_hour: left_secs_total / (60 * 60),
579 left_min: (left_secs_total / 60) % 60,
580 left_sec: left_secs_total % 60,
581 has_timelimit: contest.duration != 0,
582 is_time_left,
583 exempt_from_timelimit,
584 can_still_compete,
585 review_has_timelimit,
586 has_future_review,
587 has_review_end,
588 is_review,
589 can_still_compete_or_review: can_still_compete || is_review,
590
591 until_review_start_day: until_review_start / (60 * 60 * 24),
592 until_review_start_hour: (until_review_start / (60 * 60)) % 24,
593 until_review_start_min: (until_review_start / 60) % 60,
594
595 until_review_end_day: until_review_end / (60 * 60 * 24),
596 until_review_end_hour: (until_review_end / (60 * 60)) % 24,
597 until_review_end_min: (until_review_end / 60) % 60 }
598}
599
600pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
601 query_string: Option<String>, login_info: LoginInfo, secret: Option<String>)
602 -> MedalResult<Result<MedalValue, i32>> {
603 let session = conn.get_session_or_new(&session_token).unwrap();
604
605 if session.logincode.is_some() && session.firstname.is_none() {
606 return Err(MedalError::AccountIncomplete);
607 }
608
609 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
610
611 let mut opt_part = conn.get_participation(session.id, contest_id);
612 let mut grades = if let Some(part) = opt_part.as_ref() {
613 if let Some(team) = part.team {
614 conn.get_contest_user_grades(team, contest_id)
615 } else {
616 conn.get_contest_user_grades(session.id, contest_id)
617 }
618 } else {
619 conn.get_contest_user_grades(session.id, contest_id)
620 };
621
622 let ci = ContestInfo { id: contest.id.unwrap(),
623 name: contest.name.clone(),
624 duration: contest.duration,
625 public: contest.public,
626 requires_login: contest.requires_login.unwrap_or(false),
627 image: None,
628 language: None,
629 category: contest.category.clone(),
630 team_participation: contest.max_teamsize.map(|size| size > 1).unwrap_or(false),
631 tags: Vec::new() };
632
633 let mut data = json_val::Map::new();
634 data.insert("parent".to_string(), to_json(&"base"));
635 data.insert("empty".to_string(), to_json(&"empty"));
636 data.insert("contest".to_string(), to_json(&ci));
637 data.insert("title".to_string(), to_json(&ci.name));
638 data.insert("message".to_string(), to_json(&contest.message));
639 fill_oauth_data(login_info, &mut data);
640
641 if secret.is_some() && secret != contest.secret {
642 return Err(MedalError::AccessDenied);
643 }
644
645 let has_secret = contest.secret.is_some();
646 let mut require_secret = false;
647 if has_secret {
648 data.insert("secret_field".to_string(), to_json(&true));
649
650 if secret.is_some() {
651 data.insert("secret_field_prefill".to_string(), to_json(&secret));
652 } else {
653 require_secret = true;
654 }
655 }
656
657 let constraints = check_contest_constraints(&session, &contest);
658 let is_qualified = check_contest_qualification(conn, &session, &contest).unwrap_or(true);
659
660 let has_tasks = contest.taskgroups.len() > 0;
661 let can_start = constraints.contest_running
662 && constraints.grade_matching
663 && is_qualified
664 && (has_tasks || has_secret)
665 && (session.is_logged_in() || contest.secret.is_some() && !contest.requires_login.unwrap_or(false));
666
667 let has_duration = contest.duration > 0;
668
669 data.insert("constraints".to_string(), to_json(&constraints));
670 data.insert("is_qualified".to_string(), to_json(&is_qualified));
671 data.insert("has_duration".to_string(), to_json(&has_duration));
672 data.insert("can_start".to_string(), to_json(&can_start));
673 data.insert("has_tasks".to_string(), to_json(&has_tasks));
674 data.insert("no_tasks".to_string(), to_json(&!has_tasks));
675
676 if opt_part.is_none()
681 && contest.duration == 0
682 && constraints.contest_running
683 && constraints.grade_matching
684 && !require_secret
685 && contest.requires_login != Some(true)
686 {
687 conn.new_participation(session.id, contest_id, None).map_err(|_| MedalError::AccessDenied)?;
688 opt_part = Some(Participation { contest: contest_id,
689 user: session.id,
690 start: time::get_time(),
691 team: None,
692 annotation: None });
693 }
694
695 let now = time::get_time();
696 if let Some(start) = contest.start {
697 if now < start {
698 let until = start - now;
699 data.insert("time_until_start".to_string(),
700 to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
701 }
702 }
703
704 if let Some(end) = contest.end {
705 if now < end {
706 let until = end - now;
707 data.insert("time_until_end".to_string(),
708 to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
709 }
710 }
711
712 if session.is_logged_in() {
713 data.insert("logged_in".to_string(), to_json(&true));
714 data.insert("username".to_string(), to_json(&session.username));
715 data.insert("firstname".to_string(), to_json(&session.firstname));
716 data.insert("lastname".to_string(), to_json(&session.lastname));
717 data.insert("teacher".to_string(), to_json(&session.is_teacher));
718 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
719 }
720
721 if let Some(participation) = opt_part {
722 let time_info = check_contest_time_left(&session, &contest, &participation);
723 data.insert("time_info".to_string(), to_json(&time_info));
724
725 let time_left_formatted =
726 format!("{}:{:02}:{:02}", time_info.left_hour, time_info.left_min, time_info.left_sec);
727 data.insert("time_left_formatted".to_string(), to_json(&time_left_formatted));
728
729 if let Some(team) = participation.team {
731 if team != session.id {
733 if time_info.is_time_left {
734 data.insert("block_team_participation".to_string(), to_json(&true));
736 } else {
737 grades = conn.get_contest_user_grades(team, contest_id);
739 }
740 }
741
742 let names =
743 conn.get_team_partners_by_contest_and_teamlead(contest_id, team)
744 .iter()
745 .filter(|user| user.id != session.id)
746 .map(|user| {
747 user.firstname.clone().unwrap_or_default() + " " + &user.lastname.clone().unwrap_or_default()
748 })
749 .collect::<Vec<String>>()
750 .join(", ");
751 data.insert("team_partners".to_string(), to_json(&names));
752 }
753
754 let mut totalgrade = 0;
755 let mut max_totalgrade = 0;
756
757 let mut tasks = Vec::new();
758 for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
759 let subtaskstars = generate_subtaskstars(&taskgroup, &grade, None);
760 let ti = TaskInfo { name: taskgroup.name, subtasks: subtaskstars };
761 tasks.push(ti);
762
763 totalgrade += grade.grade.unwrap_or(0);
764 max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
765 }
766 let relative_points = if max_totalgrade > 0 { (totalgrade * 100) / max_totalgrade } else { 0 };
767
768 data.insert("tasks".to_string(), to_json(&tasks));
769
770 data.insert("is_started".to_string(), to_json(&true));
771 data.insert("total_points".to_string(), to_json(&totalgrade));
772 data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
773 data.insert("relative_points".to_string(), to_json(&relative_points));
774 data.insert("lean_page".to_string(), to_json(&true));
775
776 if has_tasks && contest.standalone_task.unwrap_or(false) {
777 return Ok(Err(tasks[0].subtasks[0].id));
778 }
779 }
780
781 if let Some(query_string) = query_string {
782 if !query_string.starts_with("bare") {
783 data.insert("not_bare".to_string(), to_json(&true));
784 }
785
786 if query_string.contains("team_participation=no_logincode") {
787 data.insert("team_error".to_string(), to_json(&"Kein Logincode angegeben"));
788 }
789 if query_string.contains("team_participation=invalid_logincode") {
790 data.insert("team_error".to_string(), to_json(&"Ungültiger Logincode angegeben"));
791 }
792 if query_string.contains("team_participation=own_logincode") {
793 data.insert("team_error".to_string(), to_json(&"Eigener Logincode angegeben"));
794 }
795 if query_string.contains("team_participation=logincode_has_participation") {
796 data.insert("team_error".to_string(), to_json(&"Logincode hat diesen Wettbewerb bereits gestartet"));
797 }
798 } else {
799 data.insert("not_bare".to_string(), to_json(&true));
800 }
801
802 Ok(Ok(("contest".to_owned(), data)))
803}
804
805pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
806 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
807 let mut data = json_val::Map::new();
808 fill_user_data(&session, &mut data);
809
810 let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);
811
812 #[derive(Serialize, Deserialize)]
813 struct UserResults {
814 firstname: String,
815 lastname: String,
816 user_id: i32,
817 grade: String,
818 logincode: String,
819 annotation: String,
820 annotation_result: String,
821 team_participants: String,
822 results: Vec<String>,
823 }
824
825 #[derive(Serialize, Deserialize)]
826 struct GroupResults {
827 groupname: String,
828 group_id: i32,
829 groupcode: String,
830 user_results: Vec<UserResults>,
831 }
832
833 let mut results: Vec<GroupResults> = Vec::new();
834 let mut has_annotations = false;
835 let mut has_annotations_result = false;
836 let mut has_teams = false;
837
838 for (group, groupdata) in resultdata {
839 let mut groupresults: Vec<UserResults> = Vec::new();
840
841 for (user, userdata) in groupdata {
842 let mut userresults: Vec<String> = Vec::new();
843
844 userresults.push(String::new());
845 let mut summe = 0;
846
847 for grade in userdata {
848 if let Some(g) = grade.grade {
849 userresults.push(format!("{}", g));
850 summe += g;
851 } else {
852 userresults.push("–".to_string());
853 }
854 }
855
856 userresults[0] = format!("{}", summe);
857
858 let (annotation, annotation_result) = if let Some(annotation) = user.annotation {
859 let mut split = annotation.split('\x1f');
860
861 (split.next()
862 .filter(|s| s.len() > 0)
863 .map(|s| {
864 has_annotations = true;
865 s.to_string()
866 })
867 .unwrap_or_default(),
868 split.next()
869 .filter(|s| s.len() > 0)
870 .map(|s| {
871 has_annotations_result = true;
872 s.to_string()
873 })
874 .unwrap_or_default())
875 } else {
876 (Default::default(), Default::default())
877 };
878
879 let team_participants = if let Some(team) = user.team {
880 has_teams = true;
881 conn.get_team_partners_by_contest_and_teamlead(contest_id, team)
882 .iter()
883 .filter(|user| user.id != session.id)
884 .map(|user| {
885 user.firstname.clone().unwrap_or_default() + " " + &user.lastname.clone().unwrap_or_default()
886 })
887 .collect::<Vec<String>>()
888 .join(", ")
889 } else {
890 Default::default()
891 };
892
893 groupresults.push(UserResults { firstname: user.firstname.unwrap_or_else(|| "–".to_string()),
894 lastname: user.lastname.unwrap_or_else(|| "–".to_string()),
895 user_id: user.id,
896 grade: grade_to_string(user.grade),
897 logincode: user.logincode.unwrap_or_else(|| "".to_string()),
898 annotation,
899 annotation_result,
900 team_participants,
901 results: userresults });
902 }
903
904 results.push(GroupResults { groupname: group.name.to_string(),
905 group_id: group.id.unwrap_or(0),
906 groupcode: group.groupcode,
907 user_results: groupresults });
908 }
909
910 data.insert("taskname".to_string(), to_json(&tasknames));
911 data.insert("result".to_string(), to_json(&results));
912 data.insert("has_annotations".to_string(), to_json(&has_annotations));
913 data.insert("has_annotations_result".to_string(), to_json(&has_annotations_result));
914 data.insert("has_teams".to_string(), to_json(&has_teams));
915
916 let c = conn.get_contest_by_id(contest_id).ok_or(MedalError::UnknownId)?;
917 let ci = ContestInfo { id: c.id.unwrap(),
918 name: c.name.clone(),
919 duration: c.duration,
920 public: c.public,
921 requires_login: c.requires_login.unwrap_or(false),
922 image: None,
923 language: None,
924 category: c.category.clone(),
925 team_participation: false,
926 tags: Vec::new() };
927
928 data.insert("contest".to_string(), to_json(&ci));
929 data.insert("contestname".to_string(), to_json(&c.name));
930
931 Ok(("contestresults".to_owned(), data))
932}
933
934pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str,
935 secret: Option<String>, logincode_team: Option<String>)
936 -> MedalResult<Result<(), String>> {
937 let session = conn.get_session_or_new(&session_token).unwrap();
939 let contest = conn.get_contest_by_id(contest_id).ok_or(MedalError::UnknownId)?;
940
941 if contest.duration != 0
943 && !session.is_logged_in()
944 && (contest.requires_login.unwrap_or(false) || contest.secret.is_none())
945 {
946 return Err(MedalError::AccessDenied);
947 }
948
949 if session.is_logged_in() && session.csrf_token != csrf_token {
951 return Err(MedalError::CsrfCheckFailed);
952 }
953
954 let constraints = check_contest_constraints(&session, &contest);
956
957 if !(constraints.contest_running && constraints.grade_matching) {
958 return Err(MedalError::AccessDenied);
959 }
960
961 let is_qualified = check_contest_qualification(conn, &session, &contest);
962
963 if is_qualified == Some(false) {
964 return Err(MedalError::AccessDenied);
965 }
966
967 if contest.secret != secret {
968 return Err(MedalError::AccessDenied);
969 }
970
971 if let Some(logincode_team) = logincode_team {
972 match contest.max_teamsize {
973 None => return Err(MedalError::AccessDenied), Some(max_teamsize) => {
975 if max_teamsize < 2 {
976 return Err(MedalError::AccessDenied); }
978 }
979 };
980
981 if logincode_team == "" {
982 return Ok(Err("no_logincode".to_string()));
983 }
984
985 let teampartner = conn.get_user_and_group_by_logincode(&logincode_team);
986 if let Some((teampartner_user, _)) = teampartner {
987 if teampartner_user.id == session.id {
988 return Ok(Err("own_logincode".to_string()));
989 }
990
991 if conn.get_participation(teampartner_user.id, contest_id).is_some() {
992 return Ok(Err("logincode_has_participation".to_string()));
993 }
994
995 if conn.get_participation(session.id, contest_id).is_some() {
996 return Err(MedalError::AccessDenied); }
998
999 let res = conn.new_participation(session.id, contest_id, Some(session.id));
1003 let _ = conn.new_participation(teampartner_user.id, contest_id, Some(session.id));
1004 return match res {
1005 Ok(_) => Ok(Ok(())),
1006 _ => Err(MedalError::AccessDenied), };
1008 } else {
1009 return Ok(Err("invalid_logincode".to_string()));
1010 }
1011 }
1012
1013 match conn.new_participation(session.id, contest_id, None) {
1015 Ok(_) => Ok(Ok(())),
1016 _ => Err(MedalError::AccessDenied), }
1018}
1019
1020pub fn login<T: MedalConnection>(conn: &T, login_data: (String, String), login_info: LoginInfo)
1021 -> Result<String, MedalValue> {
1022 let (username, password) = login_data;
1023
1024 match conn.login(None, &username, &password) {
1025 Ok(session_token) => Ok(session_token),
1026 Err(()) => {
1027 let mut data = json_val::Map::new();
1028 data.insert("reason".to_string(), to_json(&"Login fehlgeschlagen. Bitte erneut versuchen.".to_string()));
1029 data.insert("username".to_string(), to_json(&username));
1030 data.insert("parent".to_string(), to_json(&"base"));
1031
1032 fill_oauth_data(login_info, &mut data);
1033
1034 Err(("login".to_owned(), data))
1035 }
1036 }
1037}
1038
1039pub fn login_with_code<T: MedalConnection>(
1040 conn: &T, code: &str, login_info: LoginInfo)
1041 -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
1042 match conn.login_with_code(None, &code.trim()) {
1043 Ok(session_token) => Ok(Ok(session_token)),
1044 Err(()) => match conn.create_user_with_groupcode(None, &code.trim()) {
1045 Ok(session_token) => Ok(Err(session_token)),
1046 Err(()) => {
1047 let mut data = json_val::Map::new();
1048 data.insert("reason".to_string(), to_json(&"Kein gültiger Code. Bitte erneut versuchen.".to_string()));
1049 data.insert("code".to_string(), to_json(&code));
1050 data.insert("parent".to_string(), to_json(&"base"));
1051
1052 fill_oauth_data(login_info, &mut data);
1053
1054 Err(("login".to_owned(), data))
1055 }
1056 },
1057 }
1058}
1059
1060fn webauthn(self_url: &str) -> webauthn::Webauthn {
1061 use webauthn_rs::prelude::*;
1062
1063 let rp_id = self_url.split("://").nth(1).unwrap().split('/').next().unwrap().split(':').next().unwrap();
1065 let rp_origin = Url::parse(self_url).expect("Invalid URL");
1066 WebauthnBuilder::new(rp_id, &rp_origin).expect("Invalid configuration").build().expect("Invalid configuration")
1067}
1068
1069pub fn register_key_challenge<T: MedalConnection>(conn: &T, self_url: &str, session_token: &str) -> JsonValueResult {
1070 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1071
1072 let webauthn = webauthn(self_url);
1073
1074 let cred_ids = conn.get_all_webauthn_credentials();
1075
1076 let (ccr, skr) =
1078 webauthn.start_passkey_registration(webauthn::Uuid::from_u64_pair(0, session.id as u64),
1079 session.firstname.as_ref().unwrap_or(&("".to_string())),
1080 session.firstname.as_ref().unwrap_or(&("".to_string())),
1081 Some(cred_ids.into_iter()
1082 .map(|x| serde_json::from_str(&format!("\"{}\"", x)).unwrap())
1083 .collect()))
1084 .expect("Failed to start webauthn registration.");
1085
1086 conn.set_webauthn_passkey_registration(session.id, &serde_json::json!(skr).to_string());
1087
1088 let mut data = json_val::Map::new();
1089 data.insert("challenge".to_string(), to_json(&ccr));
1090 Ok(data)
1091}
1092
1093pub fn register_key<T: MedalConnection>(conn: &T, self_url: &str, session_token: &str, csrf_token: &str,
1094 credential: String, name: String)
1095 -> JsonValueResult {
1096 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1097
1098 if session.csrf_token != csrf_token {
1099 return Err(MedalError::CsrfCheckFailed);
1100 }
1101
1102 let registration: String = conn.get_webauthn_passkey_registration(session.id).ok_or(MedalError::WebauthnError)?;
1103
1104 let webauthn = webauthn(self_url);
1105
1106 let registration: webauthn::PasskeyRegistration = serde_json::from_str(®istration).unwrap();
1107 let credential: webauthn::RegisterPublicKeyCredential = serde_json::from_str(&credential).unwrap();
1108
1109 let passkey = webauthn.finish_passkey_registration(&credential, ®istration);
1110
1111 match passkey {
1112 Ok(passkey) => {
1113 if let serde_json::Value::String(cred_id) = serde_json::json!(passkey.cred_id()) {
1114 conn.add_webauthn_passkey(session.id, &cred_id, &serde_json::json!(passkey).to_string(), &name);
1115 } else {
1116 println!("Webauthn: Could not unwrap cred_id from webauthn Passkey");
1117 return Err(MedalError::WebauthnError);
1118 }
1119 }
1120 Err(webauthn::WebauthnError::UserNotVerified) => {
1121 println!("Webauthn: UserNotVerified");
1123 return Err(MedalError::WebauthnError);
1124 }
1125 Err(err) => {
1126 println!("Webauthn: Other error: {:#?}", err);
1127 return Err(MedalError::WebauthnError);
1128 }
1129 }
1130
1131 let data = json_val::Map::new();
1132 Ok(data)
1133}
1134
1135pub fn delete_key<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, token_id: i32)
1136 -> JsonValueResult {
1137 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1138
1139 if session.csrf_token != csrf_token {
1140 return Err(MedalError::CsrfCheckFailed);
1141 }
1142
1143 if !conn.get_webauthn_passkey_names_for_user(session.id).into_iter().any(|(id, _name)| id == token_id) {
1144 return Err(MedalError::NotFound);
1145 }
1146
1147 conn.delete_webauthn_passkey(session.id, token_id);
1148
1149 let data = json_val::Map::new();
1150 Ok(data)
1151}
1152
1153pub fn login_with_key_challenge<T: MedalConnection>(conn: &T, self_url: &str) -> JsonValue {
1154 let passkeys = conn.get_all_webauthn_passkeys()
1155 .into_iter()
1156 .map(|passkey| {
1157 let passkey: webauthn::Passkey = serde_json::from_str(&passkey).unwrap();
1158 passkey
1159 })
1160 .collect::<Vec<webauthn::Passkey>>();
1161
1162 let webauthn = webauthn(self_url);
1163
1164 let (challenge, authentication) = webauthn.start_passkey_authentication(&passkeys).unwrap();
1165
1166 let auth_id = conn.store_webauthn_auth_challenge(&serde_json::json!(challenge).to_string(),
1167 &serde_json::json!(authentication).to_string());
1168
1169 let mut data = json_val::Map::new();
1170 data.insert("id".to_string(), to_json(&auth_id));
1171 data.insert("challenge".to_string(), to_json(&challenge));
1172 data
1173}
1174
1175pub fn login_with_key<T: MedalConnection>(conn: &T, self_url: &str, auth_id: i32, credential: &str)
1176 -> Result<String, String> {
1177 let authentication = conn.get_webauthn_auth_challenge_by_id(auth_id).unwrap();
1178
1179 let authentication: webauthn::PasskeyAuthentication = serde_json::from_str(&authentication).unwrap();
1180 let credential: webauthn::PublicKeyCredential = serde_json::from_str(&credential).unwrap();
1181
1182 let webauthn = webauthn(self_url);
1183
1184 match webauthn.finish_passkey_authentication(&credential, &authentication) {
1185 Err(err) => {
1186 println!("Webauthn: Other error: {:#?}", err);
1187 let mut data = json_val::Map::new();
1188 data.insert("reason".to_string(),
1189 to_json(&"Unbekannter Key. Bitte Key zuerst im Profil registrieren.".to_string()));
1190 Err(serde_json::json!(data).to_string())
1191 }
1192 Ok(authresult) => {
1193 if let serde_json::Value::String(cred_id) = serde_json::json!(authresult.cred_id()) {
1194 match conn.login_with_key(None, &cred_id) {
1195 Ok(session_token) => Ok(session_token),
1196 Err(()) => {
1197 let mut data = json_val::Map::new();
1198 println!("Webauthn: Auth fail");
1199 data.insert("reason".to_string(), to_json(&"Key konnte nicht authentifiziert werden. Bitte über anderen Weg einloggen.".to_string()));
1200 Err(serde_json::json!(data).to_string())
1201 }
1202 }
1203 } else {
1204 panic!("Webauthn: Could not unwrap cred_id from webauthn AuthenticationResult")
1205 }
1206 }
1207 }
1208}
1209
1210pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
1211 session_token.map(|token| conn.logout(&token));
1212}
1213
1214#[cfg(feature = "signup")]
1215pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signup_data: (String, String, String))
1216 -> MedalResult<SignupResult> {
1217 let (username, email, password) = signup_data;
1218
1219 if username == "" || email == "" || password == "" {
1220 return Ok(SignupResult::EmptyFields);
1221 }
1222
1223 let salt = helpers::make_salt();
1224 let hash = helpers::hash_password(&password, &salt)?;
1225
1226 let result = conn.signup(&session_token.unwrap(), &username, &email, hash, &salt);
1227 Ok(result)
1228}
1229
1230#[cfg(feature = "signup")]
1231pub fn signupdata(query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
1232 let mut data = json_val::Map::new();
1233 if let Some(query) = query_string {
1234 if let Some(status) = query.strip_prefix("status=") {
1235 if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) {
1236 data.insert((status).to_string(), to_json(&true));
1237 }
1238 }
1239 }
1240 data
1241}
1242
1243pub fn load_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, subtask: Option<String>,
1244 submission_id: Option<i32>)
1245 -> MedalResult<String> {
1246 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1247
1248 match submission_id {
1249 None => match conn.load_submission(&session, task_id, subtask.as_deref()) {
1250 Some(submission) => Ok(submission.value),
1251 None => Ok("{}".to_string()),
1252 },
1253 Some(submission_id) => {
1254 let (submission, _, _, _) =
1255 conn.get_submission_by_id_complete_shallow_contest(submission_id).ok_or(MedalError::UnknownId)?;
1256
1257 if submission.user != session.id && !session.is_admin.unwrap_or(false) {
1259 if let Some((_, Some(group))) = conn.get_user_and_group_by_id(submission.user) {
1260 if !group.admins.contains(&session.id) {
1261 return Err(MedalError::AccessDenied);
1263 }
1264 } else {
1265 return Err(MedalError::AccessDenied);
1267 }
1268 }
1269 Ok(submission.value)
1270 }
1271 }
1272}
1273
1274#[allow(clippy::too_many_arguments)]
1275pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, csrf_token: &str,
1276 data: String, grade_percentage: i32, autosave: bool,
1277 subtask: Option<String>)
1278 -> MedalResult<String> {
1279 let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1280
1281 if session.csrf_token != csrf_token {
1282 return Err(MedalError::CsrfCheckFailed);
1283 }
1284
1285 let (t, _, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1286
1287 match conn.get_participation(session.id, contest.id.expect("Value from database")) {
1288 None => return Err(MedalError::AccessDenied),
1289 Some(participation) => {
1290 let time_info = check_contest_time_left(&session, &contest, &participation);
1291 if !time_info.can_still_compete && time_info.left_secs_total < -10 {
1292 return Err(MedalError::AccessDenied);
1293 }
1296 if participation.team.is_some() && participation.team != Some(session.id) {
1297 return Err(MedalError::AccessDenied);
1298 }
1299 }
1300 }
1301
1302 let grade_rounded = ((grade_percentage * t.stars * 10) / 100 + 5) / 10;
1317
1318 let submission = Submission { id: None,
1337 user: session.id,
1338 task: task_id,
1339 grade: grade_rounded,
1340 validated: false,
1341 nonvalidated_grade: grade_rounded,
1342 needs_validation: true,
1343 autosave,
1344 latest: Default::default(), highest_grade_latest: Default::default(), subtask_identifier: subtask,
1347 value: data,
1348 date: time::get_time() };
1349
1350 conn.submit_submission(submission);
1351
1352 Ok("{}".to_string())
1353}
1354
1355pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, autosaveinterval: u64)
1356 -> MedalResult<Result<MedalValue, (i32, Option<String>)>> {
1357 let session = conn.get_session_or_new(&session_token).unwrap();
1358
1359 let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1360 let grade = conn.get_taskgroup_user_grade(session.id, tg.id.unwrap()); let tasklist = conn.get_contest_by_id_complete(contest.id.unwrap()).ok_or(MedalError::UnknownId)?; let mut prevtaskgroup: Option<Taskgroup> = None;
1364 let mut nexttaskgroup: Option<Taskgroup> = None;
1365 let mut current_found = false;
1366
1367 let mut subtaskstars = Vec::new();
1368
1369 for taskgroup in tasklist.taskgroups {
1370 if current_found {
1371 nexttaskgroup = Some(taskgroup);
1372 break;
1373 }
1374
1375 if taskgroup.id == tg.id {
1376 current_found = true;
1377 subtaskstars = generate_subtaskstars(&taskgroup, &grade, Some(task_id));
1378 } else {
1379 prevtaskgroup = Some(taskgroup);
1380 }
1381 }
1382
1383 match conn.get_own_participation(session.id, contest.id.expect("Value from database")) {
1384 None => Ok(Err((contest.id.unwrap(), contest.category))),
1385 Some(participation) => {
1386 let mut data = json_val::Map::new();
1387 data.insert("subtasks".to_string(), to_json(&subtaskstars));
1388 data.insert("prevtask".to_string(), to_json(&prevtaskgroup.map(|tg| tg.tasks[0].id)));
1389 data.insert("nexttask".to_string(), to_json(&nexttaskgroup.map(|tg| tg.tasks[0].id))); let time_info = check_contest_time_left(&session, &contest, &participation);
1392 data.insert("time_info".to_string(), to_json(&time_info));
1393
1394 data.insert("time_left_mh_formatted".to_string(),
1395 to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1396 data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1397
1398 let auto_save_interval_ms = if autosaveinterval > 0 && autosaveinterval < 31536000000 {
1399 autosaveinterval * 1000
1400 } else {
1401 31536000000
1402 };
1403 data.insert("auto_save_interval_ms".to_string(), to_json(&auto_save_interval_ms));
1404
1405 if time_info.can_still_compete && participation.team.is_some() && participation.team != Some(session.id) {
1407 return Ok(Err((contest.id.unwrap(), contest.category)));
1408 }
1409
1410 if time_info.can_still_compete || time_info.is_review {
1411 data.insert("contestname".to_string(), to_json(&contest.name));
1412 data.insert("name".to_string(), to_json(&tg.name));
1413 data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1414 data.insert("taskid".to_string(), to_json(&task_id));
1415 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1416 data.insert("contestid".to_string(), to_json(&contest.id));
1417 data.insert("readonly".to_string(), to_json(&time_info.is_review));
1418 data.insert("standalone_task".to_string(), to_json(&contest.standalone_task));
1419 data.insert("category".to_string(), to_json(&contest.category));
1420
1421 let (template, tasklocation) = if let Some(language) = t.language {
1422 match language.as_str() {
1423 "blockly" => ("wtask".to_owned(), t.location.as_str()),
1424 "python" => {
1425 data.insert("tasklang".to_string(), to_json(&"python"));
1426 ("wtask".to_owned(), t.location.as_str())
1427 }
1428 _ => ("task".to_owned(), t.location.as_str()),
1429 }
1430 } else {
1431 match t.location.chars().next() {
1432 Some('B') => ("wtask".to_owned(), &t.location[1..]),
1433 Some('P') => {
1434 data.insert("tasklang".to_string(), to_json(&"python"));
1435 ("wtask".to_owned(), &t.location[1..])
1436 }
1437 _ => ("task".to_owned(), t.location.as_str()),
1438 }
1439 };
1440
1441 let taskpath = format!("{}{}", contest.location, &tasklocation);
1442 data.insert("taskpath".to_string(), to_json(&taskpath));
1443
1444 Ok(Ok((template, data)))
1445 } else {
1446 Ok(Err((contest.id.unwrap(), contest.category)))
1448 }
1449 }
1450 }
1451}
1452
1453pub fn review_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, submission_id: i32)
1454 -> MedalResult<Result<MedalValue, i32>> {
1455 let session = conn.get_session_or_new(&session_token).unwrap();
1456
1457 let (submission, t, tg, contest) =
1458 conn.get_submission_by_id_complete_shallow_contest(submission_id).ok_or(MedalError::UnknownId)?;
1459
1460 let grade = Grade { taskgroup: tg.id.unwrap(),
1463 user: session.id,
1464 grade: Some(submission.grade),
1465 validated: submission.validated };
1466
1467 if submission.user != session.id && !session.is_admin.unwrap_or(false) {
1469 if let Some((_, Some(group))) = conn.get_user_and_group_by_id(submission.user) {
1470 if !group.admins.contains(&session.id) {
1471 return Err(MedalError::AccessDenied);
1473 }
1474 } else {
1475 return Err(MedalError::AccessDenied);
1477 }
1478 }
1479
1480 let subtaskstars = generate_subtaskstars(&tg, &grade, Some(task_id)); let mut data = json_val::Map::new();
1483 data.insert("subtasks".to_string(), to_json(&subtaskstars));
1484
1485 let time_info = ContestTimeInfo { passed_secs_total: 0,
1486 left_secs_total: 0,
1487 left_mins_total: 0,
1488 left_hour: 0,
1489 left_min: 0,
1490 left_sec: 0,
1491 has_timelimit: contest.duration != 0,
1492 is_time_left: false,
1493 exempt_from_timelimit: true,
1494 can_still_compete: false,
1495 review_has_timelimit: false,
1496 has_future_review: false,
1497 has_review_end: false,
1498 is_review: true,
1499 can_still_compete_or_review: true,
1500
1501 until_review_start_day: 0,
1502 until_review_start_hour: 0,
1503 until_review_start_min: 0,
1504
1505 until_review_end_day: 0,
1506 until_review_end_hour: 0,
1507 until_review_end_min: 0 };
1508
1509 data.insert("time_info".to_string(), to_json(&time_info));
1510
1511 data.insert("time_left_mh_formatted".to_string(),
1512 to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1513 data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1514
1515 data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1516
1517 data.insert("name".to_string(), to_json(&tg.name));
1519 data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1520 data.insert("taskid".to_string(), to_json(&task_id));
1521 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1522 data.insert("readonly".to_string(), to_json(&time_info.is_review));
1524
1525 data.insert("submission".to_string(), to_json(&submission_id));
1526
1527 let (template, tasklocation) = if let Some(language) = t.language {
1528 match language.as_str() {
1529 "blockly" => ("wtask".to_owned(), t.location.as_str()),
1530 "python" => {
1531 data.insert("tasklang".to_string(), to_json(&"python"));
1532 ("wtask".to_owned(), t.location.as_str())
1533 }
1534 _ => ("task".to_owned(), t.location.as_str()),
1535 }
1536 } else {
1537 match t.location.chars().next() {
1538 Some('B') => ("wtask".to_owned(), &t.location[1..]),
1539 Some('P') => {
1540 data.insert("tasklang".to_string(), to_json(&"python"));
1541 ("wtask".to_owned(), &t.location[1..])
1542 }
1543 _ => ("task".to_owned(), t.location.as_str()),
1544 }
1545 };
1546
1547 let taskpath = format!("{}{}", contest.location, &tasklocation);
1548 data.insert("taskpath".to_string(), to_json(&taskpath));
1549
1550 Ok(Ok((template, data)))
1551}
1552
1553pub fn preview_task<T: MedalConnection>(conn: &T, task_id: i32) -> MedalResult<Result<MedalValue, i32>> {
1554 let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1555
1556 if !contest.public
1558 || contest.duration != 0
1559 || contest.requires_contest.is_some()
1560 || contest.requires_login == Some(true)
1561 || contest.standalone_task != Some(true)
1562 {
1563 return Err(MedalError::UnknownId);
1564 }
1565
1566 let time_info = ContestTimeInfo { passed_secs_total: 0,
1567 left_secs_total: 0,
1568 left_mins_total: 0,
1569 left_hour: 0,
1570 left_min: 0,
1571 left_sec: 0,
1572 has_timelimit: contest.duration != 0,
1573 is_time_left: false,
1574 exempt_from_timelimit: true,
1575 can_still_compete: false,
1576 review_has_timelimit: false,
1577 has_future_review: false,
1578 has_review_end: false,
1579 is_review: true,
1580 can_still_compete_or_review: true,
1581
1582 until_review_start_day: 0,
1583 until_review_start_hour: 0,
1584 until_review_start_min: 0,
1585
1586 until_review_end_day: 0,
1587 until_review_end_hour: 0,
1588 until_review_end_min: 0 };
1589
1590 let mut data = json_val::Map::new();
1591
1592 data.insert("time_info".to_string(), to_json(&time_info));
1593
1594 data.insert("time_left_mh_formatted".to_string(),
1595 to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1596 data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1597
1598 data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1599
1600 data.insert("contestname".to_string(), to_json(&contest.name));
1601 data.insert("name".to_string(), to_json(&tg.name));
1602 data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1603 data.insert("taskid".to_string(), to_json(&task_id));
1604 data.insert("contestid".to_string(), to_json(&contest.id));
1605 data.insert("readonly".to_string(), to_json(&time_info.is_review));
1606 data.insert("preview".to_string(), to_json(&true));
1607
1608 let (template, tasklocation) = if let Some(language) = t.language {
1609 match language.as_str() {
1610 "blockly" => ("wtask".to_owned(), t.location.as_str()),
1611 "python" => {
1612 data.insert("tasklang".to_string(), to_json(&"python"));
1613 ("wtask".to_owned(), t.location.as_str())
1614 }
1615 _ => ("task".to_owned(), t.location.as_str()),
1616 }
1617 } else {
1618 match t.location.chars().next() {
1619 Some('B') => ("wtask".to_owned(), &t.location[1..]),
1620 Some('P') => {
1621 data.insert("tasklang".to_string(), to_json(&"python"));
1622 ("wtask".to_owned(), &t.location[1..])
1623 }
1624 _ => ("task".to_owned(), t.location.as_str()),
1625 }
1626 };
1627
1628 let taskpath = format!("{}{}", contest.location, &tasklocation);
1629 data.insert("taskpath".to_string(), to_json(&taskpath));
1630
1631 Ok(Ok((template, data)))
1632}
1633
1634#[derive(Serialize, Deserialize)]
1635pub struct GroupInfo {
1636 pub id: i32,
1637 pub name: String,
1638 pub tag: String,
1639 pub code: String,
1640}
1641
1642pub fn show_groups<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1643 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1644
1645 let mut data = json_val::Map::new();
1646 fill_user_data(&session, &mut data);
1647
1648 let v: Vec<GroupInfo> =
1649 conn.get_groups(session.id)
1650 .iter()
1651 .map(|g| GroupInfo { id: g.id.unwrap(),
1652 name: g.name.clone(),
1653 tag: g.tag.clone(),
1654 code: g.groupcode.clone() })
1655 .collect();
1656 data.insert("group".to_string(), to_json(&v));
1657 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1658
1659 Ok(("groups".to_string(), data))
1660}
1661
1662#[derive(Serialize, Deserialize)]
1663pub struct MemberInfo {
1664 pub id: i32,
1665 pub firstname: String,
1666 pub lastname: String,
1667 pub sex: String,
1668 pub grade: String,
1669 pub logincode: String,
1670 pub anonymous: bool,
1671}
1672
1673pub fn show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
1674 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1675 let group = conn.get_group_complete(group_id).unwrap(); let mut data = json_val::Map::new();
1678 fill_user_data(&session, &mut data);
1679
1680 if !group.admins.contains(&session.id) {
1681 return Err(MedalError::AccessDenied);
1682 }
1683
1684 let gi = GroupInfo { id: group.id.unwrap(),
1685 name: group.name.clone(),
1686 tag: group.tag.clone(),
1687 code: group.groupcode.clone() };
1688
1689 let v: Vec<MemberInfo> = group.members
1690 .iter()
1691 .filter_map(|m| {
1692 Some(MemberInfo { id: m.id,
1693 firstname: m.firstname.clone()?,
1694 lastname: m.lastname.clone()?,
1695 sex: (match m.sex {
1696 Some(0) | None => "/",
1697 Some(1) => "m",
1698 Some(2) => "w",
1699 Some(3) => "d",
1700 Some(4) => "…",
1701 _ => "?",
1702 }).to_string(),
1703 grade: grade_to_string(m.grade),
1704 logincode: m.logincode.clone()?,
1705 anonymous: m.anonymous })
1706 })
1707 .collect();
1708
1709 data.insert("group".to_string(), to_json(&gi));
1710 data.insert("member".to_string(), to_json(&v));
1711 data.insert("groupname".to_string(), to_json(&gi.name));
1712
1713 Ok(("group".to_string(), data))
1714}
1715
1716pub fn add_group<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, name: String, tag: String)
1717 -> MedalResult<i32> {
1718 let session = conn.get_session(&session_token)
1719 .ensure_logged_in()
1720 .ok_or(MedalError::AccessDenied)?
1721 .ensure_teacher_or_admin()
1722 .ok_or(MedalError::AccessDenied)?;
1723
1724 if session.csrf_token != csrf_token {
1725 return Err(MedalError::CsrfCheckFailed);
1726 }
1727
1728 let mut groupcode = String::new();
1729 for i in 0..10 {
1730 if i == 9 {
1731 panic!("ERROR: Too many groupcode collisions! Give up ...");
1732 }
1733 groupcode = helpers::make_groupcode();
1734 if !conn.code_exists(&groupcode) {
1735 break;
1736 }
1737 println!("WARNING: Groupcode collision! Retrying ...");
1738 }
1739
1740 let mut group = Group { id: None, name, groupcode, tag, admins: vec![session.id], members: Vec::new() };
1741
1742 conn.add_group(&mut group);
1743
1744 Ok(group.id.unwrap())
1745}
1746
1747pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str, sex_infos: SexInformation) -> MedalValueResult {
1748 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1749
1750 let mut data = json_val::Map::new();
1751 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1752
1753 data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1754 data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1755 data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1756 data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1757
1758 Ok(("groupcsv".to_string(), data))
1759}
1760
1761pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
1763 -> MedalResult<()> {
1764 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1765
1766 if session.csrf_token != csrf_token {
1767 return Err(MedalError::CsrfCheckFailed);
1768 }
1769
1770 let mut v: Vec<Vec<String>> = serde_json::from_str(group_data).or(Err(MedalError::AccessDenied))?; v.sort_unstable_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1772
1773 let mut groupcode = String::new();
1774 let mut name = String::new();
1775 let mut group = Group { id: None,
1776 name: name.clone(),
1777 groupcode,
1778 tag: String::new(),
1779 admins: vec![session.id],
1780 members: Vec::new() };
1781
1782 for line in v {
1783 if name != line[0] {
1784 if name != "" {
1785 conn.update_or_create_group_with_users(group, session.id);
1786 }
1787 name = line[0].clone();
1788
1789 groupcode = String::new();
1790 for i in 0..10 {
1791 if i == 9 {
1792 panic!("ERROR: Too many groupcode collisions! Give up ...");
1793 }
1794 groupcode = helpers::make_groupcode();
1795 if !conn.code_exists(&groupcode) {
1796 break;
1797 }
1798 println!("WARNING: Groupcode collision! Retrying ...");
1799 }
1800
1801 group = Group { id: None,
1802 name: name.clone(),
1803 groupcode,
1804 tag: name.clone(),
1805 admins: vec![session.id],
1806 members: Vec::new() };
1807 }
1808
1809 let mut user = SessionUser::group_user_stub();
1810 user.grade = line[1].parse::<i32>().unwrap_or(0);
1811 user.firstname = Some(line[2].clone());
1812 user.lastname = Some(line[3].clone());
1813
1814 use db_objects::Sex;
1815 match line[4].as_str() {
1816 "m" => user.sex = Some(Sex::Male as i32),
1817 "f" => user.sex = Some(Sex::Female as i32),
1818 "d" => user.sex = Some(Sex::Diverse as i32),
1819 _ => user.sex = None,
1820 }
1821
1822 user.anonymous = line[5] == "y";
1823
1824 group.members.push(user);
1825 }
1826 conn.update_or_create_group_with_users(group, session.id);
1827
1828 Ok(())
1829}
1830
1831pub fn contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1832 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1833
1834 let mut data = json_val::Map::new();
1835 data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1836
1837 Ok(("admin_admissioncsv".to_string(), data))
1838}
1839
1840pub fn upload_contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
1841 contest_id: i32, admission_data: &str)
1842 -> MedalResult<()> {
1843 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1844
1845 if session.csrf_token != csrf_token {
1846 return Err(MedalError::CsrfCheckFailed);
1847 }
1848
1849 let v: Vec<Vec<String>> = serde_json::from_str(admission_data).or(Err(MedalError::AccessDenied))?; let w: Vec<(i32, Option<String>)> = v.into_iter()
1852 .map(|vv| {
1853 (vv[0].parse().unwrap_or(-1),
1854 if vv[1].len() == 0 && vv[2].len() == 0 && vv[3].len() == 0 {
1855 None
1856 } else {
1857 Some(format!("{}\x1f{}\x1f{}", vv[1], vv[2], vv[3]))
1859 })
1860 })
1861 .collect();
1862
1863 let _annotations_inserted = conn.insert_contest_annotations(contest_id, w);
1864
1865 Ok(())
1866}
1867
1868#[allow(dead_code)]
1869pub fn show_groups_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
1870 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1871 let _g = conn.get_contest_groups_grades(session.id, contest_id);
1873
1874 let data = json_val::Map::new();
1875
1876 Ok(("groupresults".into(), data))
1877}
1878
1879pub struct SexInformation {
1880 pub require_sex: bool,
1881 pub allow_sex_na: bool,
1882 pub allow_sex_diverse: bool,
1883 pub allow_sex_other: bool,
1884}
1885
1886pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
1887 query_string: Option<String>, sex_infos: SexInformation)
1888 -> MedalValueResult {
1889 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1890
1891 let mut data = json_val::Map::new();
1892 fill_user_data(&session, &mut data);
1893
1894 data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1895 data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1896 data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1897 data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1898
1899 match user_id {
1900 None => {
1901 data.insert("profile_firstname".to_string(), to_json(&session.firstname));
1902 data.insert("profile_lastname".to_string(), to_json(&session.lastname));
1903 data.insert("profile_street".to_string(), to_json(&session.street));
1904 data.insert("profile_zip".to_string(), to_json(&session.zip));
1905 data.insert("profile_city".to_string(), to_json(&session.city));
1906 data.insert(format!("sel{}", session.grade), to_json(&"selected"));
1907 if let Some(sex) = session.sex {
1908 data.insert(format!("sex_{}", sex), to_json(&"selected"));
1909 } else {
1910 data.insert("sex_None".to_string(), to_json(&"selected"));
1911 }
1912
1913 data.insert("profile_logincode".to_string(), to_json(&session.logincode));
1914 if session.password.is_some() {
1915 data.insert("profile_username".to_string(), to_json(&session.username));
1916 }
1917 if session.managed_by.is_none() {
1918 data.insert("profile_not_in_group".into(), to_json(&true));
1919 }
1920 if session.oauth_provider != Some("pms".to_string()) {
1921 data.insert("profile_not_pms".into(), to_json(&true));
1922 }
1925 data.insert("ownprofile".into(), to_json(&true));
1926 data.insert("userid".into(), to_json(&session.id));
1927
1928 let webauthn = conn.get_webauthn_passkey_names_for_user(session.id);
1929 data.insert("webauthn".into(), to_json(&webauthn));
1930 data.insert("has_webauthn".into(), to_json(&!webauthn.is_empty()));
1931
1932 if let Some(query) = query_string {
1933 if let Some(status) = query.strip_prefix("status=") {
1934 if ["NothingChanged",
1935 "DataChanged",
1936 "PasswordChanged",
1937 "PasswordMissmatch",
1938 "firstlogin",
1939 "SignedUp"].contains(&status)
1940 {
1941 data.insert((status).to_string(), to_json(&true));
1942 }
1943 }
1944 }
1945
1946 let now = time::get_time();
1947
1948 let participations: (Vec<(i32, String, bool, bool, bool, Option<String>)>,
1950 Vec<(i32, String, bool, bool, bool, Option<String>)>) =
1951 conn.get_all_participations_complete(session.id)
1952 .into_iter()
1953 .rev()
1954 .map(|(participation, contest)| {
1955 let passed_secs = now.sec - participation.start.sec;
1956 let left_secs = i64::from(contest.duration) * 60 - passed_secs;
1957 let is_time_left = contest.duration == 0 || left_secs >= 0;
1958 let has_timelimit = contest.duration != 0;
1959 let requires_login = contest.requires_login == Some(true);
1960 let annotation =
1961 participation.annotation
1962 .map(|annotation| annotation.split('\x1f').nth(2).unwrap_or("").to_string());
1963
1964 (contest.id.unwrap(), contest.name, has_timelimit, is_time_left, requires_login, annotation)
1965 })
1966 .partition(|contest| contest.2 && !contest.4);
1967 data.insert("participations".into(), to_json(&participations));
1968
1969 let stars_count = conn.count_all_stars(session.id);
1970 data.insert("stars_count".into(), to_json(&stars_count));
1971 let stars_message = match stars_count {
1972 0 => "Auf gehts, dein erster Stern wartet auf dich!",
1973 1..=9 => "Ein hervorragender Anfang!",
1974 10..=99 => "Das ist ziemlich gut!",
1975 100..=999 => "Ein wahrer Meister!",
1976 _ => "Wow! Einfach wow!",
1977 }.to_string();
1978
1979 data.insert("stars_message".into(), to_json(&stars_message));
1980 }
1981 Some(user_id) => {
1983 let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
1985 let group = opt_group.ok_or(MedalError::AccessDenied)?;
1986 if !group.admins.contains(&session.id) {
1987 return Err(MedalError::AccessDenied);
1988 }
1989
1990 data.insert("profile_firstname".to_string(), to_json(&user.firstname));
1991 data.insert("profile_lastname".to_string(), to_json(&user.lastname));
1992 data.insert("profile_street".to_string(), to_json(&session.street));
1993 data.insert("profile_zip".to_string(), to_json(&session.zip));
1994 data.insert("profile_city".to_string(), to_json(&session.city));
1995 data.insert(format!("sel{}", user.grade), to_json(&"selected"));
1996 if let Some(sex) = user.sex {
1997 data.insert(format!("sex_{}", sex), to_json(&"selected"));
1998 } else {
1999 data.insert("sex_None".to_string(), to_json(&"selected"));
2000 }
2001
2002 data.insert("profile_logincode".to_string(), to_json(&user.logincode));
2003 if user.username.is_some() {
2004 data.insert("profile_username".to_string(), to_json(&user.username));
2005 }
2006 if user.managed_by.is_none() {
2007 data.insert("profile_not_in_group".into(), to_json(&true));
2008 }
2009 if session.oauth_provider != Some("pms".to_string()) {
2010 data.insert("profile_not_pms".into(), to_json(&true));
2011 }
2012 data.insert("ownprofile".into(), to_json(&false));
2013
2014 if let Some(query) = query_string {
2015 if let Some(status) = query.strip_prefix("status=") {
2016 if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
2017 data.insert((status).to_string(), to_json(&true));
2018 }
2019 }
2020 }
2021 }
2022 }
2023
2024 Ok(("profile".to_string(), data))
2025}
2026
2027#[derive(Debug, PartialEq, Eq)]
2028pub enum ProfileStatus {
2029 NothingChanged,
2030 DataChanged,
2031 PasswordChanged,
2032 PasswordMissmatch,
2033}
2034impl From<ProfileStatus> for String {
2035 fn from(s: ProfileStatus) -> String { format!("{:?}", s) }
2036}
2037
2038pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str,
2039 (firstname,
2040 lastname,
2041 street,
2042 zip,
2043 city,
2044 password,
2045 password_repeat,
2046 grade,
2047 sex): (String,
2048 String,
2049 Option<String>,
2050 Option<String>,
2051 Option<String>,
2052 Option<String>,
2053 Option<String>,
2054 i32,
2055 Option<i32>))
2056 -> MedalResult<ProfileStatus> {
2057 let mut session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2058
2059 if session.csrf_token != csrf_token {
2060 return Err(MedalError::AccessDenied); }
2062
2063 let mut result = ProfileStatus::NothingChanged;
2064
2065 let mut password_and_salt = None;
2066
2067 if let (Some(password), Some(password_repeat)) = (password, password_repeat) {
2068 if password != "" || password_repeat != "" {
2069 if password == password_repeat {
2070 let salt = helpers::make_salt();
2071 let hash = helpers::hash_password(&password, &salt)?;
2072
2073 password_and_salt = Some((hash, salt));
2074 result = ProfileStatus::PasswordChanged;
2075 } else {
2076 result = ProfileStatus::PasswordMissmatch;
2077 }
2078 }
2079 }
2080
2081 if result == ProfileStatus::NothingChanged {
2082 if session.firstname.as_ref() == Some(&firstname)
2083 && session.lastname.as_ref() == Some(&lastname)
2084 && session.street == street
2085 && session.zip == zip
2086 && session.city == city
2087 && session.grade == grade
2088 && session.sex == sex
2089 {
2090 return Ok(ProfileStatus::NothingChanged);
2091 } else {
2092 result = ProfileStatus::DataChanged;
2093 }
2094 }
2095
2096 match user_id {
2097 None => {
2098 session.firstname = Some(firstname);
2099 session.lastname = Some(lastname);
2100 session.grade = grade;
2101 session.sex = sex;
2102
2103 if street.is_some() {
2104 session.street = street;
2105 }
2106 if zip.is_some() {
2107 session.zip = zip;
2108 }
2109 if city.is_some() {
2110 session.city = city;
2111 }
2112
2113 if let Some((password, salt)) = password_and_salt {
2114 session.password = Some(password);
2115 session.salt = Some(salt);
2116 }
2117
2118 conn.save_session(session);
2119 }
2120 Some(user_id) => {
2121 let (mut user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2123 let group = opt_group.ok_or(MedalError::AccessDenied)?;
2124 if !group.admins.contains(&session.id) {
2125 return Err(MedalError::AccessDenied);
2126 }
2127
2128 user.firstname = Some(firstname);
2129 user.lastname = Some(lastname);
2130 user.grade = grade;
2131 user.sex = sex;
2132
2133 if street.is_some() {
2134 user.street = street;
2135 }
2136 if zip.is_some() {
2137 user.zip = zip;
2138 }
2139 if city.is_some() {
2140 user.city = city;
2141 }
2142
2143 if let Some((password, salt)) = password_and_salt {
2144 user.password = Some(password);
2145 user.salt = Some(salt);
2146 }
2147
2148 conn.save_session(user);
2149 }
2150 }
2151
2152 Ok(result)
2153}
2154
2155pub fn check_profile<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
2156 (firstname, lastname): (String, String))
2157 -> JsonValueResult {
2158 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2159
2160 if session.csrf_token != csrf_token {
2161 return Err(MedalError::AccessDenied);
2162 }
2163
2164 let mut data = json_val::Map::new();
2165
2166 if session.firstname.map(|n| n.len() > 0).unwrap_or(false) && session.lastname.map(|n| n.len() > 0).unwrap_or(false)
2168 {
2169 data.insert("exists".to_string(), to_json(&false));
2170 return Ok(data);
2171 }
2172
2173 let group =
2174 conn.get_group_complete(session.managed_by.ok_or(MedalError::AccessDenied)?).ok_or(MedalError::AccessDenied)?;
2175
2176 for member in group.members {
2177 if member.lastname.map(|l| l.to_lowercase()) == Some(lastname.to_lowercase())
2178 && member.firstname.map(|l| l.to_lowercase()) == Some(firstname.to_lowercase())
2179 && member.id != session.id
2180 {
2181 data.insert("exists".to_string(), to_json(&true));
2182 return Ok(data);
2183 }
2184 }
2185
2186 data.insert("exists".to_string(), to_json(&false));
2187 Ok(data)
2188}
2189
2190pub fn teacher_infos<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2191 let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2192 if !session.is_teacher {
2193 return Err(MedalError::AccessDenied);
2194 }
2195
2196 let mut data = json_val::Map::new();
2197 fill_user_data(&session, &mut data);
2198
2199 Ok(("teacher".to_string(), data))
2200}
2201
2202pub fn admin_index<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2203 let session = conn.get_session(&session_token)
2204 .ensure_logged_in()
2205 .ok_or(MedalError::NotLoggedIn)?
2206 .ensure_admin()
2207 .ok_or(MedalError::AccessDenied)?;
2208
2209 let mut data = json_val::Map::new();
2210 fill_user_data(&session, &mut data);
2211
2212 Ok(("admin".to_string(), data))
2213}
2214
2215pub fn admin_search_users<T: MedalConnection>(conn: &T, session_token: &str,
2216 s_data: (Option<i32>,
2217 Option<String>,
2218 Option<String>,
2219 Option<String>,
2220 Option<String>,
2221 Option<String>))
2222 -> MedalValueResult {
2223 let session = conn.get_session(&session_token)
2224 .ensure_logged_in()
2225 .ok_or(MedalError::NotLoggedIn)?
2226 .ensure_admin()
2227 .ok_or(MedalError::AccessDenied)?;
2228
2229 let mut data = json_val::Map::new();
2230 fill_user_data(&session, &mut data);
2231
2232 match conn.get_search_users(s_data) {
2233 Ok(users) => {
2234 data.insert("users".to_string(), to_json(&users));
2235 data.insert("max_results".to_string(), to_json(&200));
2236 data.insert("num_results".to_string(), to_json(&users.len()));
2237 data.insert("no_results".to_string(), to_json(&(users.len() == 0)));
2238 if users.len() > 200 {
2239 data.insert("more_users".to_string(), to_json(&true));
2240 data.insert("more_results".to_string(), to_json(&true));
2241 }
2242 }
2243 Err(groups) => {
2244 data.insert("groups".to_string(), to_json(&groups));
2245 data.insert("max_results".to_string(), to_json(&200));
2246 data.insert("num_results".to_string(), to_json(&groups.len()));
2247 data.insert("no_results".to_string(), to_json(&(groups.len() == 0)));
2248 if groups.len() > 200 {
2249 data.insert("more_groups".to_string(), to_json(&true));
2250 data.insert("more_results".to_string(), to_json(&true));
2251 }
2252 }
2253 };
2254
2255 Ok(("admin_search_results".to_string(), data))
2256}
2257
2258pub fn admin_show_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str) -> MedalValueResult {
2259 let session = conn.get_session(&session_token)
2260 .ensure_logged_in()
2261 .ok_or(MedalError::NotLoggedIn)?
2262 .ensure_teacher_or_admin()
2263 .ok_or(MedalError::AccessDenied)?;
2264
2265 let mut data = json_val::Map::new();
2266
2267 let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2268
2269 if !session.is_admin() {
2270 if let Some(group) = opt_group.clone() {
2272 if !group.admins.contains(&session.id) {
2273 return Err(MedalError::AccessDenied);
2274 }
2275 } else if user_id != session.id {
2276 return Err(MedalError::AccessDenied);
2277 }
2278 }
2279
2280 fill_user_data(&session, &mut data);
2281 fill_user_data_prefix(&user, &mut data, "user_");
2282 data.insert("user_logincode".to_string(), to_json(&user.logincode));
2283 data.insert("user_id".to_string(), to_json(&user.id));
2284 let grade = if user.grade >= 200 {
2285 "Kein Schüler mehr".to_string()
2286 } else if user.grade >= 11 {
2287 format!("{} ({})", user.grade % 100, if user.grade >= 100 { "G9" } else { "G8" })
2288 } else {
2289 format!("{}", user.grade)
2290 };
2291 data.insert("user_grade".to_string(), to_json(&grade));
2292 data.insert("user_oauthid".to_string(), to_json(&user.oauth_foreign_id));
2293 data.insert("user_oauthprovider".to_string(), to_json(&user.oauth_provider));
2294
2295 if let Some(group) = opt_group {
2296 data.insert("user_group_id".to_string(), to_json(&group.id));
2297 data.insert("user_group_name".to_string(), to_json(&group.name));
2298 }
2299
2300 let groups: Vec<GroupInfo> =
2301 conn.get_groups(user_id)
2302 .iter()
2303 .map(|g| GroupInfo { id: g.id.unwrap(),
2304 name: g.name.clone(),
2305 tag: g.tag.clone(),
2306 code: g.groupcode.clone() })
2307 .collect();
2308 data.insert("user_group".to_string(), to_json(&groups));
2309
2310 let parts = conn.get_all_participations_complete(user_id);
2311 let has_protected_participations = parts.iter().any(|p| p.1.protected);
2312 let contest_stars = conn.count_all_stars_by_contest(user_id);
2313
2314 let pi: Vec<(i32, String, String, i32)> =
2315 parts.into_iter()
2316 .map(|(p, c)| {
2317 (c.id.unwrap(),
2318 format!("{}{}{}",
2319 &c.name,
2320 if c.protected { " (geschützt)" } else { "" },
2321 if p.team.is_some() { " (Teamteilnahme)" } else { "" }),
2322 self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(p.start)).unwrap(),
2323 contest_stars.iter()
2324 .filter_map(|(id, stars)| if *id == c.id.unwrap() { Some(*stars) } else { None })
2325 .next()
2326 .unwrap_or(0))
2327 })
2328 .collect();
2329
2330 data.insert("user_participations".to_string(), to_json(&pi));
2331 data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2332 data.insert("can_delete".to_string(),
2333 to_json(&((!has_protected_participations || session.is_admin()) && groups.len() == 0)));
2334
2335 Ok(("admin_user".to_string(), data))
2336}
2337
2338pub fn admin_delete_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str, csrf_token: &str)
2339 -> JsonValueResult {
2340 let session = conn.get_session(&session_token)
2341 .ensure_logged_in()
2342 .ok_or(MedalError::NotLoggedIn)?
2343 .ensure_teacher_or_admin()
2344 .ok_or(MedalError::AccessDenied)?;
2345
2346 if session.csrf_token != csrf_token {
2347 return Err(MedalError::CsrfCheckFailed);
2348 }
2349
2350 let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2351
2352 if !session.is_admin() {
2353 if let Some(group) = opt_group {
2355 if !group.admins.contains(&session.id) {
2356 return Err(MedalError::AccessDenied);
2357 }
2358 } else {
2359 return Err(MedalError::AccessDenied);
2360 }
2361 }
2362
2363 let parts = conn.get_all_participations_complete(user_id);
2364 let has_protected_participations = parts.iter().any(|p| p.1.protected);
2365 let groups = conn.get_groups(user_id);
2366
2367 let mut data = json_val::Map::new();
2368 if has_protected_participations && !session.is_admin() {
2369 data.insert("reason".to_string(), to_json(&"Benutzer hat Teilnahmen an geschützten Wettbewerben."));
2370 Err(MedalError::ErrorWithJson(data))
2371 } else if groups.len() > 0 {
2372 data.insert("reason".to_string(), to_json(&"Benutzer ist Administrator von Gruppen."));
2373 Err(MedalError::ErrorWithJson(data))
2374 } else {
2375 conn.delete_user(user_id);
2376 Ok(data)
2377 }
2378}
2379
2380pub fn admin_move_user_to_group<T: MedalConnection>(conn: &T, user_id: i32, group_id: i32, session_token: &str,
2381 csrf_token: &str)
2382 -> JsonValueResult {
2383 let session = conn.get_session(&session_token)
2384 .ensure_logged_in()
2385 .ok_or(MedalError::NotLoggedIn)?
2386 .ensure_admin()
2387 .ok_or(MedalError::AccessDenied)?;
2388
2389 if session.csrf_token != csrf_token {
2390 return Err(MedalError::CsrfCheckFailed);
2391 }
2392
2393 let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2394
2395 if !session.is_admin() {
2396 if let Some(group) = opt_group {
2398 if !group.admins.contains(&session.id) {
2399 return Err(MedalError::AccessDenied);
2400 }
2401 } else {
2402 return Err(MedalError::AccessDenied);
2403 }
2404 }
2405
2406 let mut data = json_val::Map::new();
2407 if conn.get_group(group_id).is_some() {
2408 if let Some(mut user) = conn.get_user_by_id(user_id) {
2409 user.managed_by = Some(group_id);
2410 conn.save_session(user);
2411 Ok(data)
2412 } else {
2413 data.insert("reason".to_string(), to_json(&"Benutzer existiert nicht."));
2414 Err(MedalError::ErrorWithJson(data))
2415 }
2416 } else {
2417 data.insert("reason".to_string(), to_json(&"Gruppe existiert nicht."));
2418 Err(MedalError::ErrorWithJson(data))
2419 }
2420}
2421
2422#[derive(Serialize, Deserialize)]
2423pub struct AdminInfo {
2424 pub id: i32,
2425 pub firstname: String,
2426 pub lastname: String,
2427}
2428
2429pub fn admin_show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2430 let session = conn.get_session(&session_token)
2431 .ensure_logged_in()
2432 .ok_or(MedalError::NotLoggedIn)?
2433 .ensure_teacher_or_admin()
2434 .ok_or(MedalError::AccessDenied)?;
2435
2436 let group = conn.get_group_complete(group_id).unwrap(); if !session.is_admin() {
2439 if !group.admins.contains(&session.id) {
2441 return Err(MedalError::AccessDenied);
2442 }
2443 }
2444
2445 let mut data = json_val::Map::new();
2446 fill_user_data(&session, &mut data);
2447
2448 let gi = GroupInfo { id: group.id.unwrap(),
2449 name: group.name.clone(),
2450 tag: group.tag.clone(),
2451 code: group.groupcode.clone() };
2452
2453 let v: Vec<MemberInfo> =
2454 group.members
2455 .iter()
2456 .filter(|m| session.is_admin() || m.firstname.is_some() || m.lastname.is_some())
2457 .map(|m| MemberInfo { id: m.id,
2458 firstname: m.firstname.clone().unwrap_or_else(|| "".to_string()),
2459 lastname: m.lastname.clone().unwrap_or_else(|| "".to_string()),
2460 sex: (match m.sex {
2461 Some(0) | None => "/",
2462 Some(1) => "m",
2463 Some(2) => "w",
2464 Some(3) => "d",
2465 Some(4) => "…",
2466 _ => "?",
2467 }).to_string(),
2468 grade: grade_to_string(m.grade),
2469 logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()),
2470 anonymous: m.anonymous })
2471 .collect();
2472
2473 let has_anonmous_members = group.members.iter().any(|m| m.anonymous);
2474
2475 let has_protected_participations = conn.group_has_protected_participations(group_id);
2476
2477 data.insert("group".to_string(), to_json(&gi));
2478 data.insert("member".to_string(), to_json(&v));
2479 data.insert("groupname".to_string(), to_json(&gi.name));
2480 data.insert("has_anonymous_members".to_string(), to_json(&has_anonmous_members));
2481 data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2482 data.insert("can_delete".to_string(), to_json(&(!has_protected_participations || session.is_admin())));
2483
2484 let admins: Vec<AdminInfo> =
2485 group.admins
2486 .iter()
2487 .map(|a| {
2488 let admin = conn.get_user_by_id(*a).ok_or(MedalError::AccessDenied)?;
2489 Ok(AdminInfo { id: admin.id,
2490 firstname: admin.firstname.clone().unwrap_or_else(|| "".to_string()),
2491 lastname: admin.lastname.clone().unwrap_or_else(|| "".to_string()) })
2492 })
2493 .collect::<Result<Vec<_>, _>>()?;
2494
2495 data.insert("group_admin".to_string(), to_json(&admins));
2496 data.insert("morethanoneadmin".to_string(), to_json(&(admins.len() > 1)));
2497
2498 Ok(("admin_group".to_string(), data))
2499}
2500
2501pub fn group_add_admin<T: MedalConnection>(conn: &T, group_id: i32, new_admin_id: i32, session_token: &str,
2502 csrf_token: &str)
2503 -> JsonValueResult {
2504 let session = conn.get_session(&session_token)
2505 .ensure_logged_in()
2506 .ok_or(MedalError::NotLoggedIn)?
2507 .ensure_teacher_or_admin()
2508 .ok_or(MedalError::AccessDenied)?;
2509
2510 if session.csrf_token != csrf_token {
2511 return Err(MedalError::CsrfCheckFailed);
2512 }
2513
2514 let mut group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2517 if !group.admins.contains(&session.id) {
2519 return Err(MedalError::AccessDenied);
2520 }
2521 }
2522
2523 if group.admins.contains(&new_admin_id) {
2524 let mut data = json_val::Map::new();
2525 data.insert("reason".to_string(), to_json(&"Benutzer ist bereits Admin."));
2526 return Err(MedalError::ErrorWithJson(data));
2527 }
2528
2529 let new_admin = conn.get_user_by_id(new_admin_id).ok_or(MedalError::NotFound)?;
2530 let first_admin_id = group.admins.first().ok_or(MedalError::NotFound)?;
2531 let first_admin = conn.get_user_by_id(*first_admin_id).ok_or(MedalError::NotFound)?;
2532 if new_admin.oauth_provider == first_admin.oauth_provider {
2533 if let Some((_, new_admin_school)) = new_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2534 {
2535 if let Some((_, first_admin_school)) =
2536 first_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2537 {
2538 if new_admin_school == first_admin_school && new_admin_school.len() >= 1 {
2539 conn.add_admin_to_group(&mut group, new_admin_id);
2540
2541 let data = json_val::Map::new();
2542 return Ok(data);
2543 }
2544 }
2545 }
2546 }
2547
2548 let mut data = json_val::Map::new();
2549 data.insert("reason".to_string(),
2550 to_json(&"Benutzer gehört nicht zur gleichen Schule oder Benutzer nicht als Lehrkraft angemeldet."));
2551 Err(MedalError::ErrorWithJson(data))
2552}
2553
2554pub fn group_delete_admin<T: MedalConnection>(conn: &T, group_id: i32, admin_id: i32, session_token: &str,
2555 csrf_token: &str)
2556 -> JsonValueResult {
2557 let session = conn.get_session(&session_token)
2558 .ensure_logged_in()
2559 .ok_or(MedalError::NotLoggedIn)?
2560 .ensure_teacher_or_admin()
2561 .ok_or(MedalError::AccessDenied)?;
2562
2563 if session.csrf_token != csrf_token {
2564 return Err(MedalError::CsrfCheckFailed);
2565 }
2566
2567 let mut group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2570 if !group.admins.contains(&session.id) {
2572 return Err(MedalError::AccessDenied);
2573 }
2574 }
2575
2576 if group.admins.len() == 1 {
2577 let mut data = json_val::Map::new();
2578 data.insert("reason".to_string(), to_json(&"Kann letzten Admin nicht entfernen."));
2579 return Err(MedalError::ErrorWithJson(data));
2580 }
2581
2582 if !group.admins.contains(&admin_id) {
2583 let mut data = json_val::Map::new();
2584 data.insert("reason".to_string(), to_json(&"Benutzer ist kein Admin."));
2585 return Err(MedalError::ErrorWithJson(data));
2586 }
2587
2588 conn.remove_admin_from_group(&mut group, admin_id);
2589 let data = json_val::Map::new();
2590 Ok(data)
2591}
2592
2593pub fn admin_delete_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str)
2594 -> JsonValueResult {
2595 let session = conn.get_session(&session_token)
2596 .ensure_logged_in()
2597 .ok_or(MedalError::NotLoggedIn)?
2598 .ensure_teacher_or_admin()
2599 .ok_or(MedalError::AccessDenied)?;
2600
2601 if session.csrf_token != csrf_token {
2602 return Err(MedalError::CsrfCheckFailed);
2603 }
2604
2605 let group = conn.get_group(group_id).unwrap(); if !session.is_admin() {
2608 if !group.admins.contains(&session.id) {
2610 return Err(MedalError::AccessDenied);
2611 }
2612 }
2613
2614 let mut data = json_val::Map::new();
2615 if conn.group_has_protected_participations(group_id) && !session.is_admin() {
2616 data.insert("reason".to_string(), to_json(&"Gruppe hat Mitglieder mit geschützten Teilnahmen."));
2617 Err(MedalError::ErrorWithJson(data))
2618 } else {
2619 conn.delete_all_users_for_group(group_id);
2620 conn.delete_group(group_id);
2621 Ok(data)
2622 }
2623}
2624
2625pub fn admin_show_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2626 let session = conn.get_session(&session_token)
2627 .ensure_logged_in()
2628 .ok_or(MedalError::NotLoggedIn)?
2629 .ensure_teacher_or_admin()
2630 .ok_or(MedalError::AccessDenied)?;
2631
2632 let group = conn.get_group_complete(group_id).unwrap(); if !session.is_admin() {
2635 if !group.admins.contains(&session.id) {
2637 return Err(MedalError::AccessDenied);
2638 }
2639 }
2640
2641 let mut data = json_val::Map::new();
2642 fill_user_data(&session, &mut data);
2643
2644 let gi = GroupInfo { id: group.id.unwrap(),
2645 name: group.name.clone(),
2646 tag: group.tag.clone(),
2647 code: group.groupcode.clone() };
2648
2649 data.insert("group".to_string(), to_json(&gi));
2650 data.insert("groupname".to_string(), to_json(&gi.name));
2651 data.insert("grouptag".to_string(), to_json(&gi.name));
2652
2653 Ok(("admin_edit_group".to_string(), data))
2654}
2655
2656pub fn admin_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str,
2657 name: String, tag: String)
2658 -> MedalResult<()> {
2659 let session = conn.get_session(&session_token)
2660 .ensure_logged_in()
2661 .ok_or(MedalError::NotLoggedIn)?
2662 .ensure_teacher_or_admin()
2663 .ok_or(MedalError::AccessDenied)?;
2664
2665 if session.csrf_token != csrf_token {
2666 return Err(MedalError::CsrfCheckFailed);
2667 }
2668
2669 let mut group = conn.get_group_complete(group_id).unwrap(); if !session.is_admin() {
2672 if !group.admins.contains(&session.id) {
2674 return Err(MedalError::AccessDenied);
2675 }
2676 }
2677
2678 group.name = name;
2679 group.tag = tag;
2680
2681 conn.save_group(&mut group);
2682
2683 Ok(())
2684}
2685
2686#[derive(Serialize, Deserialize, Debug)]
2687struct SubmissionResult {
2688 id: i32,
2689 grade: i32,
2690 date: String,
2691}
2692#[derive(Serialize, Deserialize, Debug)]
2693struct TaskResult {
2694 id: i32,
2695 stars: i32,
2696 submissions: Vec<SubmissionResult>,
2697}
2698#[derive(Serialize, Deserialize, Debug)]
2699struct TaskgroupResult {
2700 id: i32,
2701 name: String,
2702 tasks: Vec<TaskResult>,
2703}
2704
2705pub fn admin_show_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str)
2706 -> MedalValueResult {
2707 let session = conn.get_session(&session_token)
2708 .ensure_logged_in()
2709 .ok_or(MedalError::NotLoggedIn)?
2710 .ensure_teacher_or_admin()
2711 .ok_or(MedalError::AccessDenied)?;
2712
2713 let user = conn.get_user_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2714 let participation = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
2715
2716 let user_or_team = if let Some(team) = participation.team { team } else { user_id };
2717
2718 let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2719
2720 if !session.is_admin() {
2721 if let Some(ref group) = opt_group {
2723 if !group.admins.contains(&session.id) {
2724 return Err(MedalError::AccessDenied);
2725 }
2726 } else {
2727 return Err(MedalError::AccessDenied);
2728 }
2729 }
2730
2731 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2732 let grades = conn.get_contest_user_grades(user_or_team, contest_id);
2733
2734 let mut totalgrade = 0;
2735 let mut max_totalgrade = 0;
2736
2737 for taskgroup in &contest.taskgroups {
2738 max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
2739 }
2740 for grade in grades {
2741 totalgrade += grade.grade.unwrap_or(0);
2742 }
2743
2744 #[rustfmt::skip]
2745 let subms: Vec<TaskgroupResult> =
2746 contest.taskgroups
2747 .into_iter()
2748 .map(|tg| TaskgroupResult {
2749 id: tg.id.unwrap(),
2750 name: tg.name,
2751 tasks: tg.tasks
2752 .into_iter()
2753 .map(|t| TaskResult {
2754 id: t.id.unwrap(),
2755 stars: t.stars,
2756 submissions: conn.get_all_submissions(user_or_team, t.id.unwrap(), None)
2757 .into_iter()
2758 .map(|s| SubmissionResult {
2759 id: s.id.unwrap(),
2760 grade: s.grade,
2761 date: self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(s.date)).unwrap(),
2762 })
2763 .collect(),
2764 })
2765 .collect(),
2766 })
2767 .collect();
2768
2769 let mut data = json_val::Map::new();
2770
2771 data.insert("submissions".to_string(), to_json(&subms));
2772 data.insert("contestid".to_string(), to_json(&contest.id));
2773 data.insert("contestname".to_string(), to_json(&contest.name));
2774 data.insert("has_timelimit".to_string(), to_json(&(contest.duration > 0)));
2775
2776 data.insert("total_grade".to_string(), to_json(&totalgrade));
2777 data.insert("max_total_grade".to_string(), to_json(&max_totalgrade));
2778
2779 if let Some(group) = opt_group {
2780 data.insert("group_id".to_string(), to_json(&group.id));
2781 data.insert("group_name".to_string(), to_json(&group.name));
2782 }
2783
2784 fill_user_data(&session, &mut data);
2785 fill_user_data_prefix(&user, &mut data, "user_");
2786 data.insert("user_id".to_string(), to_json(&user.id));
2787
2788 data.insert("start_date".to_string(),
2789 to_json(&self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(participation.start)).unwrap()));
2790
2791 data.insert("can_delete".to_string(), to_json(&(!contest.protected || session.is_admin.unwrap_or(false))));
2792 Ok(("admin_participation".to_string(), data))
2793}
2794
2795pub fn admin_delete_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str,
2796 csrf_token: &str)
2797 -> JsonValueResult {
2798 let session = conn.get_session(&session_token)
2799 .ensure_logged_in()
2800 .ok_or(MedalError::NotLoggedIn)?
2801 .ensure_teacher_or_admin()
2802 .ok_or(MedalError::AccessDenied)?;
2803
2804 if session.csrf_token != csrf_token {
2805 return Err(MedalError::CsrfCheckFailed);
2806 }
2807
2808 let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2809 let _part = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
2810 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2811
2812 if !session.is_admin() {
2813 if contest.protected {
2815 return Err(MedalError::AccessDenied);
2816 }
2817
2818 if let Some(group) = opt_group {
2819 if !group.admins.contains(&session.id) {
2820 return Err(MedalError::AccessDenied);
2821 }
2822 } else {
2823 return Err(MedalError::AccessDenied);
2824 }
2825 }
2826
2827 let mut data = json_val::Map::new();
2828 fill_user_data(&session, &mut data);
2829
2830 conn.delete_participation(user_id, contest_id);
2831 Ok(data)
2832}
2833
2834pub fn admin_show_contests<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2835 let session = conn.get_session(&session_token)
2836 .ensure_logged_in()
2837 .ok_or(MedalError::NotLoggedIn)?
2838 .ensure_admin()
2839 .ok_or(MedalError::AccessDenied)?;
2840
2841 let mut data = json_val::Map::new();
2842 fill_user_data(&session, &mut data);
2843
2844 let mut contests: Vec<_> = conn.get_contest_list().into_iter().map(|contest| (contest.id, contest.name)).collect();
2845 contests.sort(); contests.reverse();
2847
2848 data.insert("contests".to_string(), to_json(&contests));
2849
2850 Ok(("admin_contests".to_string(), data))
2851}
2852
2853pub fn admin_contest_export<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalResult<String> {
2854 conn.get_session(&session_token)
2855 .ensure_logged_in()
2856 .ok_or(MedalError::NotLoggedIn)?
2857 .ensure_admin()
2858 .ok_or(MedalError::AccessDenied)?;
2859
2860 let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2861
2862 let taskgroup_ids: Vec<(i32, String)> =
2863 contest.taskgroups.into_iter().map(|tg| (tg.id.unwrap(), tg.name)).collect();
2864 let filename = format!("contest_{}__{}__{}.csv",
2865 contest_id,
2866 self::time::strftime("%F_%H-%M-%S", &self::time::now()).unwrap(),
2867 helpers::make_filename_secret());
2868
2869 conn.export_contest_results_to_file(contest_id, &taskgroup_ids, &format!("./export/{}", filename));
2870
2871 Ok(filename)
2872}
2873
2874pub fn admin_show_cleanup<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2875 let session = conn.get_session(&session_token)
2876 .ensure_logged_in()
2877 .ok_or(MedalError::NotLoggedIn)?
2878 .ensure_admin()
2879 .ok_or(MedalError::AccessDenied)?;
2880
2881 let mut data = json_val::Map::new();
2882 fill_user_data(&session, &mut data);
2883
2884 let now = time::get_time();
2885 let maxage = now - time::Duration::days(30); let n_temporary_session = conn.count_temporary_sessions(maxage);
2887 data.insert("temporary_session_count".to_string(), to_json(&n_temporary_session));
2888
2889 Ok(("admin_cleanup".to_string(), data))
2890}
2891
2892pub fn admin_do_cleanup<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str) -> JsonValueResult {
2893 let session = conn.get_session(&session_token)
2894 .ensure_logged_in()
2895 .ok_or(MedalError::NotLoggedIn)?
2896 .ensure_admin()
2897 .ok_or(MedalError::AccessDenied)?;
2898
2899 if session.csrf_token != csrf_token {
2900 return Err(MedalError::CsrfCheckFailed);
2901 }
2902
2903 let now = time::get_time();
2904 let maxstudentage = now - time::Duration::days(180); let maxteacherage = now - time::Duration::days(1095); let maxage = now - time::Duration::days(3650); let result = conn.remove_old_users_and_groups(maxstudentage, Some(maxteacherage), Some(maxage));
2909
2910 let mut data = json_val::Map::new();
2911 if let Ok((n_user, n_group, n_teacher, n_other)) = result {
2912 data.insert("n_user".to_string(), to_json(&n_user));
2913 data.insert("n_group".to_string(), to_json(&n_group));
2914 data.insert("n_teacher".to_string(), to_json(&n_teacher));
2915 data.insert("n_other".to_string(), to_json(&n_other));
2916 Ok(data)
2917 } else {
2918 data.insert("reason".to_string(), to_json(&"Datenbank-Fehler."));
2919 Err(MedalError::ErrorWithJson(data))
2920 }
2921}
2922
2923pub fn do_session_cleanup<T: MedalConnection>(conn: &T) -> JsonValueResult {
2924 let now = time::get_time();
2925 let maxage = now - time::Duration::days(30); conn.remove_temporary_sessions(maxage, Some(1000));
2928
2929 let data = json_val::Map::new();
2930 Ok(data)
2931}
2932
2933pub fn move_task_location<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, old_location: &str,
2934 new_location: &str, contest: Option<i32>)
2935 -> JsonValueResult {
2936 let session = conn.get_session(&session_token)
2937 .ensure_logged_in()
2938 .ok_or(MedalError::NotLoggedIn)?
2939 .ensure_admin()
2940 .ok_or(MedalError::AccessDenied)?;
2941
2942 if session.csrf_token != csrf_token {
2943 return Err(MedalError::CsrfCheckFailed);
2944 }
2945
2946 let mut data = json_val::Map::new();
2947 if old_location == new_location {
2948 data.insert("reason".to_string(), to_json(&"old and new location identical"));
2949 return Err(MedalError::ErrorWithJson(data));
2950 }
2951
2952 let n_contest = conn.move_task_location(old_location, new_location, contest);
2953
2954 data.insert("contests_modified".to_string(), to_json(&n_contest));
2955
2956 Ok(data)
2957}
2958
2959#[derive(PartialEq, Eq, Clone, Copy)]
2960pub enum UserType {
2961 User,
2962 Teacher,
2963 Admin,
2964}
2965
2966pub enum UserSex {
2967 Female,
2968 Male,
2969 Unknown,
2970}
2971
2972pub struct ForeignUserData {
2973 pub foreign_id: String,
2974 pub foreign_type: UserType,
2975 pub sex: UserSex,
2976 pub firstname: String,
2977 pub lastname: String,
2978 pub school_name: Option<String>,
2979}
2980
2981pub fn login_oauth<T: MedalConnection>(conn: &T, user_data: ForeignUserData, oauth_provider_id: String,
2982 autoclean_submissions: bool)
2983 -> Result<(String, bool), (String, json_val::Map<String, json_val::Value>)> {
2984 match conn.login_foreign(None,
2985 &oauth_provider_id,
2986 &user_data.foreign_id,
2987 (user_data.foreign_type != UserType::User,
2988 user_data.foreign_type == UserType::Admin,
2989 &user_data.firstname,
2990 &user_data.lastname,
2991 match user_data.sex {
2992 UserSex::Male => Some(1),
2993 UserSex::Female => Some(2),
2994 UserSex::Unknown => Some(0),
2995 },
2996 &user_data.school_name))
2997 {
2998 Ok((session_token, last_activity)) => {
2999 let redirect_profile = if let Some(last_activity) = last_activity {
3000 let now = time::get_time();
3001 now - last_activity > time::Duration::days(60)
3002 } else {
3003 true
3004 };
3005
3006 let now = time::get_time();
3010 let maxage = now - time::Duration::days(30);
3011 conn.remove_temporary_sessions(maxage, Some(200)); if autoclean_submissions {
3014 let maxage = now - time::Duration::days(90);
3019 conn.remove_all_but_latest_submissions(maxage, Some(10_000)); }
3021
3022 Ok((session_token, redirect_profile))
3023 }
3024 Err(()) => {
3025 let mut data = json_val::Map::new();
3026 data.insert("reason".to_string(), to_json(&"OAuth-Login failed.".to_string()));
3027 Err(("login".to_owned(), data))
3028 }
3029 }
3030}