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