Owner paper route plus take-paper CTA on shared pages
GET /paper/:id redirects the owning user to their attempt screen (404 otherwise, login wall otherwise). Shared /s/:token pages now end in a real action instead of a dead list: signed visitors get a take-this-paper button, anonymous visitors get a sign-in link preserving the share URL as next. Verified live: 1/302/1 across the three states.
This commit is contained in:
parent
231dd0385e
commit
f796c566b9
2 changed files with 40 additions and 5 deletions
25
src/main.rs
25
src/main.rs
|
|
@ -79,6 +79,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
.route("/history", get(history_page))
|
||||
.route("/dashboard", get(dashboard_page))
|
||||
.route("/s/{token}", get(shared_paper))
|
||||
.route("/paper/{id}", get(paper_page))
|
||||
.route("/api/attempt/{attempt}/state", get(attempt_state))
|
||||
.route("/api/attempt/{attempt}/submit", post(submit_api))
|
||||
.nest_service("/static", get_service(ServeDir::new(static_dir)))
|
||||
|
|
@ -575,7 +576,26 @@ async fn dashboard_page(State(ctx): State<CtxArc>, h: HeaderMap) -> axum::respon
|
|||
hx(views::dashboard(&ctx, &name, &tests, &weak, ¬es, (tests.len() as i32, marks, gained, lost), &mistakes), StatusCode::OK).into_response()
|
||||
}
|
||||
|
||||
async fn shared_paper(State(ctx): State<CtxArc>, Path(token): Path<String>) -> axum::response::Response {
|
||||
/// Owner view of a paper: redirects to the attempt screen so the paper can
|
||||
/// be taken. Private — only the owning user may resolve their paper id.
|
||||
async fn paper_page(State(ctx): State<CtxArc>, h: HeaderMap, Path(id): Path<String>) -> axum::response::Response {
|
||||
let (uid, _name) = match ctx.require_user(&h).await {
|
||||
Ok(v) => v,
|
||||
Err(r) => return r,
|
||||
};
|
||||
let owned: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM papers WHERE id=? AND user_id=?")
|
||||
.bind(&id)
|
||||
.bind(&uid)
|
||||
.fetch_one(&ctx.pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
if owned == 0 {
|
||||
return hx(views::error(&ctx, 404, ""), StatusCode::NOT_FOUND).into_response();
|
||||
}
|
||||
Redirect::to(&format!("{}/attempt/{id}", ctx.base)).into_response()
|
||||
}
|
||||
|
||||
async fn shared_paper(State(ctx): State<CtxArc>, h: HeaderMap, Path(token): Path<String>) -> axum::response::Response {
|
||||
let row = sqlx::query("SELECT id, title, duration_min FROM papers WHERE share_token=?").bind(&token).fetch_optional(&ctx.pool).await;
|
||||
let (id, title, duration) = match row {
|
||||
Ok(Some(r)) => {
|
||||
|
|
@ -588,7 +608,8 @@ async fn shared_paper(State(ctx): State<CtxArc>, Path(token): Path<String>) -> a
|
|||
Ok(v) => v,
|
||||
Err(_) => return hx(views::error(&ctx, 500, ""), StatusCode::INTERNAL_SERVER_ERROR).into_response(),
|
||||
};
|
||||
hx(views::shared(&ctx, &title, duration, &questions), StatusCode::OK).into_response()
|
||||
let signed = auth::user_id(&ctx.pool, &h).await.is_some();
|
||||
hx(views::shared(&ctx, &title, duration, &questions, &id, &token, signed), StatusCode::OK).into_response()
|
||||
}
|
||||
|
||||
const TERMS: &str = include_str!("../content/terms.html");
|
||||
|
|
|
|||
20
src/views.rs
20
src/views.rs
|
|
@ -904,7 +904,7 @@ pub fn dashboard(ctx: &Ctx, user: &str, tests: &[TestRow], weak: &[(Bucket, Stri
|
|||
shell(ctx, Some(user), "Dashboard", body)
|
||||
}
|
||||
|
||||
pub fn shared(ctx: &Ctx, title: &str, duration: i64, questions: &[Question]) -> String {
|
||||
pub fn shared(ctx: &Ctx, title: &str, duration: i64, questions: &[Question], paper_id: &str, token: &str, signed: bool) -> String {
|
||||
let qs: String = questions
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
|
@ -928,12 +928,26 @@ pub fn shared(ctx: &Ctx, title: &str, duration: i64, questions: &[Question]) ->
|
|||
)
|
||||
})
|
||||
.collect();
|
||||
let cta = if signed {
|
||||
format!(
|
||||
r#"<form method="post" action="{b}/attempt/{id}" style="margin:16px 0"><button class="btn primary block" type="submit">take this paper</button></form>"#,
|
||||
b = ctx.base,
|
||||
id = esc(paper_id)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"<p style="margin:16px 0"><a class="btn primary" href="{b}/student?next=/s/{tok}">sign in to take this paper</a></p>"#,
|
||||
b = ctx.base,
|
||||
tok = esc(token)
|
||||
)
|
||||
};
|
||||
let body = format!(
|
||||
r#"<h1>{title}</h1><p class="meta">{count} questions · {dur} minutes · no answers or scores on this page.</p><div style="margin-top:16px">{qs}</div>"#,
|
||||
r#"<h1>{title}</h1><p class="meta">{count} questions · {dur} minutes · no answers or scores on this page.</p>{cta}<div style="margin-top:16px">{qs}</div>"#,
|
||||
title = esc(title),
|
||||
count = questions.len(),
|
||||
dur = duration,
|
||||
qs = qs
|
||||
qs = qs,
|
||||
cta = cta
|
||||
);
|
||||
shell(ctx, None, title, body)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue