From d7050fed23dbc28271ec0975ba29e5dd47e04d2b Mon Sep 17 00:00:00 2001 From: Robert Czechowski <robert@code-intelligence.com> Date: Sun, 23 Jun 2024 17:02:56 +0200 Subject: [PATCH] Add new task preview that allows standalone tasks to be viewed without a session / without cookies set --- src/core.rs | 81 +++++++++++++++++++++++++++++++++++++ src/tests.rs | 69 +++++++++++++++++++++++++++++++ src/webfw_iron.rs | 19 +++++++++ templates/default/wtask.hbs | 13 +++++- 4 files changed, 181 insertions(+), 1 deletion(-) diff --git a/src/core.rs b/src/core.rs index 1583d6a5..f6820606 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1210,6 +1210,87 @@ pub fn review_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &s Ok(Ok((template, data))) } +pub fn preview_task<T: MedalConnection>(conn: &T, task_id: i32) -> MedalResult<Result<MedalValue, i32>> { + let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?; + + // Require a public, accessible standalone task + if !contest.public + || contest.duration != 0 + || contest.requires_contest.is_some() + || contest.requires_login == Some(true) + || contest.standalone_task != Some(true) + { + return Err(MedalError::UnknownId); + } + + let time_info = ContestTimeInfo { passed_secs_total: 0, + left_secs_total: 0, + left_mins_total: 0, + left_hour: 0, + left_min: 0, + left_sec: 0, + has_timelimit: contest.duration != 0, + is_time_left: false, + exempt_from_timelimit: true, + can_still_compete: false, + review_has_timelimit: false, + has_future_review: false, + has_review_end: false, + is_review: true, + can_still_compete_or_review: true, + + until_review_start_day: 0, + until_review_start_hour: 0, + until_review_start_min: 0, + + until_review_end_day: 0, + until_review_end_hour: 0, + until_review_end_min: 0 }; + + let mut data = json_val::Map::new(); + + data.insert("time_info".to_string(), to_json(&time_info)); + + data.insert("time_left_mh_formatted".to_string(), + to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min))); + data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec))); + + data.insert("auto_save_interval_ms".to_string(), to_json(&0)); + + data.insert("contestname".to_string(), to_json(&contest.name)); + data.insert("name".to_string(), to_json(&tg.name)); + data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name))); + data.insert("taskid".to_string(), to_json(&task_id)); + data.insert("contestid".to_string(), to_json(&contest.id)); + data.insert("readonly".to_string(), to_json(&time_info.is_review)); + data.insert("preview".to_string(), to_json(&true)); + + let (template, tasklocation) = if let Some(language) = t.language { + match language.as_str() { + "blockly" => ("wtask".to_owned(), t.location.as_str()), + "python" => { + data.insert("tasklang".to_string(), to_json(&"python")); + ("wtask".to_owned(), t.location.as_str()) + } + _ => ("task".to_owned(), t.location.as_str()), + } + } else { + match t.location.chars().next() { + Some('B') => ("wtask".to_owned(), &t.location[1..]), + Some('P') => { + data.insert("tasklang".to_string(), to_json(&"python")); + ("wtask".to_owned(), &t.location[1..]) + } + _ => ("task".to_owned(), t.location.as_str()), + } + }; + + let taskpath = format!("{}{}", contest.location, &tasklocation); + data.insert("taskpath".to_string(), to_json(&taskpath)); + + Ok(Ok((template, data))) +} + #[derive(Serialize, Deserialize)] pub struct GroupInfo { pub id: i32, diff --git a/src/tests.rs b/src/tests.rs index 681c6542..731b03c3 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -302,6 +302,37 @@ fn run<P, F>(p: P, f: F) contest.taskgroups.push(taskgroup); contest.save(&conn); + // ID: 8 + let mut contest = Contest { id: None, + location: "directory".to_string(), + filename: "standalone.yaml".to_string(), + name: "StandaloneTaskName".to_string(), + duration: 0, // Time: Unlimited + public: true, + start: None, + end: None, + review_start: None, + review_end: None, + min_grade: None, + max_grade: None, + positionalnumber: None, + protected: false, + requires_login: None, + requires_contest: None, + secret: None, + message: None, + image: None, + language: None, + category: None, + standalone_task: Some(true), + tags: Vec::new(), + taskgroups: Vec::new() }; + let mut taskgroup = Taskgroup::new("StandaloneTaskName".to_string(), None); + let task = Task::new("standalonetaskdir".to_string(), None, 3); // ID: 15 + taskgroup.tasks.push(task); + contest.taskgroups.push(taskgroup); + contest.save(&conn); + let mut config = config::read_config_from_file(Path::new("thisfileshoudnotexist.json")); let port = { @@ -2014,3 +2045,41 @@ fn check_group_csv_upload_update() { assert_eq!(logincode2, logincode); }) } + +#[test] +fn check_preview_standalone_task() { + run(|_| {}, + |port| { + let client = reqwest::Client::builder().cookie_store(false) + .redirect(reqwest::RedirectPolicy::none()) + .build() + .unwrap(); + + let resp = client.pget(port, "preview/1").send().unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let resp = client.pget(port, "preview/3").send().unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let resp = client.pget(port, "preview/5").send().unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let mut resp = client.pget(port, "preview/15").send().unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let content = resp.text().unwrap(); + assert!(content.contains("<em>Review-Modus</em>")); + + let resp = client.pget(port, "task/1").send().unwrap(); + assert_eq!(resp.status(), StatusCode::FOUND); + + let resp = client.pget(port, "task/3").send().unwrap(); + assert_eq!(resp.status(), StatusCode::FOUND); + + let resp = client.pget(port, "task/5").send().unwrap(); + assert_eq!(resp.status(), StatusCode::FOUND); + + let resp = client.pget(port, "task/15").send().unwrap(); + assert_eq!(resp.status(), StatusCode::FOUND); + }) +} diff --git a/src/webfw_iron.rs b/src/webfw_iron.rs index 810b251d..53fbcbca 100644 --- a/src/webfw_iron.rs +++ b/src/webfw_iron.rs @@ -813,6 +813,24 @@ fn review<C>(req: &mut Request) -> IronResult<Response> } } +fn preview<C>(req: &mut Request) -> IronResult<Response> + where C: MedalConnection + std::marker::Send + 'static { + let task_id = req.expect_int::<i32>("taskid")?; + + match with_conn![core::preview_task, C, req, task_id].aug(req)? { + Ok((template, data)) => { + let mut resp = Response::new(); + resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); + Ok(resp) + } + Err(contest_id) => { + // Idea: Append task, and if contest can be started immediately, we can just redirect again! + Ok(Response::with((status::Found, + Redirect(url_for!(req, "contest", "contestid" => format!("{}",contest_id)))))) + } + } +} + fn groups<C>(req: &mut Request) -> IronResult<Response> where C: MedalConnection + std::marker::Send + 'static { let session_token = req.require_session_token()?; @@ -1648,6 +1666,7 @@ pub fn start_server<C>(conn: C, config: Config) -> iron::error::HttpResult<iron: profile_post: post "/profile/:userid" => user_post::<C>, task: get "/task/:taskid" => task::<C>, task_review_solution: get "/task/:taskid/:submissionid" => review::<C>, + preview_task: get "/preview/:taskid" => preview::<C>, teacher: get "/teacher" => teacherinfos::<C>, admin: get "/admin" => admin::<C>, admin_users: post "/admin/user/" => admin_users::<C>, diff --git a/templates/default/wtask.hbs b/templates/default/wtask.hbs index ee8cf5b3..8cf30df1 100644 --- a/templates/default/wtask.hbs +++ b/templates/default/wtask.hbs @@ -34,7 +34,11 @@ margin:0px; background: #8ca405; color:white; font-size:12pt; +{{#if preview}} +height: 0px; +{{else}} height: 35px; +{{/if}} overflow-y:hidden; } @@ -97,8 +101,12 @@ background: #f5fbe8; iframe { width: 100vw; border: 0px; +{{#if preview}} +min-height: 100vh; /* Fallback for browsers that do not support Custom Properties */ +{{else}} min-height: calc(100vh - 35px); /* Fallback for browsers that do not support Custom Properties */ min-height: calc(var(--vh) - 35px); +{{/if}} } a { @@ -354,7 +362,10 @@ function getTaskProxyCallback(task) { {{#if submission}} window.load_submission_object({{submission}}, load_task_callback, load_task_error); {{else}} - window.load_task_object(load_task_callback, load_task_error); + {{#if preview}} + {{else}} + window.load_task_object(load_task_callback, load_task_error); + {{/if}} {{/if}} // } -- GitLab