Previous-year-paper mode: year+shift picker builds the full actual paper (exam.rs past branch, build-page mode cards wired)
This commit is contained in:
parent
78f4ac2507
commit
e4c51126a0
4 changed files with 161 additions and 1 deletions
64
src/exam.rs
64
src/exam.rs
|
|
@ -62,6 +62,12 @@ pub struct BuildForm {
|
||||||
/// "jee" means 3 subjects × 60 min each; otherwise one flat timer.
|
/// "jee" means 3 subjects × 60 min each; otherwise one flat timer.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
/// When "1", take every question of the given year+shift (past-paper mode).
|
||||||
|
#[serde(default)]
|
||||||
|
pub past: String,
|
||||||
|
/// Shift filter for past-paper mode ("S1" or "S2"); "ALL" means any.
|
||||||
|
#[serde(default)]
|
||||||
|
pub shift: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuildForm {
|
impl BuildForm {
|
||||||
|
|
@ -87,9 +93,16 @@ impl BuildForm {
|
||||||
pub fn is_jee_mode(&self) -> bool {
|
pub fn is_jee_mode(&self) -> bool {
|
||||||
self.mode == "jee"
|
self.mode == "jee"
|
||||||
}
|
}
|
||||||
|
pub fn is_past_mode(&self) -> bool {
|
||||||
|
self.past == "1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn build_paper(pool: &SqlitePool, uid: &str, f: &BuildForm) -> anyhow::Result<(String, String)> {
|
pub async fn build_paper(pool: &SqlitePool, uid: &str, f: &BuildForm) -> anyhow::Result<(String, String)> {
|
||||||
|
// Past-paper mode: every question of one year+shift, no chapter picking.
|
||||||
|
if f.is_past_mode() {
|
||||||
|
return build_past_paper(pool, uid, f).await;
|
||||||
|
}
|
||||||
let chapters = f.chapter_ids();
|
let chapters = f.chapter_ids();
|
||||||
if chapters.is_empty() {
|
if chapters.is_empty() {
|
||||||
anyhow::bail!("Pick at least one chapter");
|
anyhow::bail!("Pick at least one chapter");
|
||||||
|
|
@ -432,4 +445,53 @@ pub async fn finalize(pool: &SqlitePool, attempt: &str) -> anyhow::Result<(i32,
|
||||||
}
|
}
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
Ok((score, max, used))
|
Ok((score, max, used))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Past-paper mode: take every question from the chosen year (and shift when
|
||||||
|
/// given), in a stable year-major order — mirrors sitting an actual paper.
|
||||||
|
async fn build_past_paper(pool: &SqlitePool, uid: &str, f: &BuildForm) -> anyhow::Result<(String, String)> {
|
||||||
|
let years = f.year_list();
|
||||||
|
let year = match years.first() {
|
||||||
|
Some(y) => *y,
|
||||||
|
None => anyhow::bail!("Pick a year for the past paper"),
|
||||||
|
};
|
||||||
|
let shift = f.shift.trim();
|
||||||
|
let (sql, binds): (&str, Vec<String>) = if shift.is_empty() || shift == "ALL" {
|
||||||
|
("SELECT id FROM questions WHERE year=? ORDER BY subject, id", vec![year.to_string()])
|
||||||
|
} else {
|
||||||
|
("SELECT id FROM questions WHERE year=? AND shift=? ORDER BY subject, id", vec![year.to_string(), shift.to_string()])
|
||||||
|
};
|
||||||
|
let mut q = sqlx::query_scalar::<_, i64>(sql);
|
||||||
|
for b in &binds { q = q.bind(b); }
|
||||||
|
let mut ids: Vec<i64> = q.fetch_all(pool).await?;
|
||||||
|
if ids.is_empty() {
|
||||||
|
anyhow::bail!("No questions in the bank for that year and shift.");
|
||||||
|
}
|
||||||
|
ids.sort_unstable();
|
||||||
|
ids.dedup();
|
||||||
|
// Past papers are not shuffled: the real paper's order is part of the
|
||||||
|
// practice. Cap at 75 for the same reason as build mode.
|
||||||
|
ids.truncate(75);
|
||||||
|
|
||||||
|
let paper = uuid::Uuid::new_v4().to_string();
|
||||||
|
let share = uuid::Uuid::new_v4().simple().to_string()[..10].to_string();
|
||||||
|
let title = if shift.is_empty() || shift == "ALL" {
|
||||||
|
format!("JEE Main {year}")
|
||||||
|
} else {
|
||||||
|
format!("JEE Main {year} {shift}")
|
||||||
|
};
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
sqlx::query("INSERT INTO papers(id, user_id, title, duration_min, created_at, share_token) VALUES(?,?,?,?,?,?)")
|
||||||
|
.bind(&paper).bind(uid).bind(&title).bind(180).bind(chrono::Utc::now().to_rfc3339()).bind(&share)
|
||||||
|
.execute(&mut *tx).await?;
|
||||||
|
for (ord, qid) in ids.iter().enumerate() {
|
||||||
|
sqlx::query("INSERT INTO paper_questions(paper_id, question_id, ord) VALUES(?,?,?)")
|
||||||
|
.bind(&paper).bind(qid).bind(ord as i64)
|
||||||
|
.execute(&mut *tx).await?;
|
||||||
|
sqlx::query("INSERT OR REPLACE INTO seen_questions(user_id, question_id, seen_at) VALUES(?,?,?)")
|
||||||
|
.bind(uid).bind(qid).bind(chrono::Utc::now().to_rfc3339())
|
||||||
|
.execute(&mut *tx).await?;
|
||||||
|
}
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok((paper, share))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,8 @@ async fn build_page(State(ctx): State<CtxArc>, h: HeaderMap) -> axum::response::
|
||||||
duration: Some(30),
|
duration: Some(30),
|
||||||
unseen_only: Some("on".into()),
|
unseen_only: Some("on".into()),
|
||||||
mode: String::new(),
|
mode: String::new(),
|
||||||
|
past: String::new(),
|
||||||
|
shift: String::new(),
|
||||||
};
|
};
|
||||||
hx(views::build(&ctx, &name, blocks, &form, available, None), StatusCode::OK).into_response()
|
hx(views::build(&ctx, &name, blocks, &form, available, None), StatusCode::OK).into_response()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
74
src/views.rs
74
src/views.rs
|
|
@ -496,6 +496,25 @@ pub fn build(ctx: &Ctx, user: &str, blocks: Vec<SubjectBlock>, _form: &BuildForm
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Past-paper picker (hidden until Previous-year mode) -->
|
||||||
|
<div class="zp-past-panel" id="zp-past-panel" hidden>
|
||||||
|
<span class="zp-filter-heading">PICK A YEAR</span>
|
||||||
|
<div class="zp-pill-row" id="past-year-row">
|
||||||
|
<label class="zp-filter-pill"><input type="radio" name="past-year" value="2025"><span>2025</span></label>
|
||||||
|
<label class="zp-filter-pill"><input type="radio" name="past-year" value="2024"><span>2024</span></label>
|
||||||
|
<label class="zp-filter-pill"><input type="radio" name="past-year" value="2023"><span>2023</span></label>
|
||||||
|
<label class="zp-filter-pill"><input type="radio" name="past-year" value="2022"><span>2022</span></label>
|
||||||
|
<label class="zp-filter-pill"><input type="radio" name="past-year" value="2021"><span>2021</span></label>
|
||||||
|
<label class="zp-filter-pill"><input type="radio" name="past-year" value="2020"><span>2020</span></label>
|
||||||
|
</div>
|
||||||
|
<span class="zp-filter-heading" style="margin-top:14px">SHIFT</span>
|
||||||
|
<div class="zp-pill-row" id="past-shift-row">
|
||||||
|
<label class="zp-filter-pill active"><input type="radio" name="past-shift" value="ALL" checked><span>any</span></label>
|
||||||
|
<label class="zp-filter-pill"><input type="radio" name="past-shift" value="S1"><span>shift 1</span></label>
|
||||||
|
<label class="zp-filter-pill"><input type="radio" name="past-shift" value="S2"><span>shift 2</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="zp-complete-btn" id="btn-complete-syllabus">complete syllabus</button>
|
<button type="button" class="zp-complete-btn" id="btn-complete-syllabus">complete syllabus</button>
|
||||||
<p class="zp-complete-hint">Selects every chapter in all three subjects. You can still remove any you don't want.</p>
|
<p class="zp-complete-hint">Selects every chapter in all three subjects. You can still remove any you don't want.</p>
|
||||||
|
|
||||||
|
|
@ -537,6 +556,8 @@ pub fn build(ctx: &Ctx, user: &str, blocks: Vec<SubjectBlock>, _form: &BuildForm
|
||||||
<input type="hidden" name="difficulties" id="difficulties" value="Easy,Medium,Hard">
|
<input type="hidden" name="difficulties" id="difficulties" value="Easy,Medium,Hard">
|
||||||
<input type="hidden" name="years" id="years" value="2020,2021,2022,2023,2024,2025">
|
<input type="hidden" name="years" id="years" value="2020,2021,2022,2023,2024,2025">
|
||||||
<input type="hidden" name="mode" id="mode" value="jee">
|
<input type="hidden" name="mode" id="mode" value="jee">
|
||||||
|
<input type="hidden" name="past" id="past" value="">
|
||||||
|
<input type="hidden" name="shift" id="shift" value="">
|
||||||
|
|
||||||
<!-- Chapter Rows Container -->
|
<!-- Chapter Rows Container -->
|
||||||
<div class="zp-chapters-container">
|
<div class="zp-chapters-container">
|
||||||
|
|
@ -616,6 +637,47 @@ pub fn build(ctx: &Ctx, user: &str, blocks: Vec<SubjectBlock>, _form: &BuildForm
|
||||||
const btnDesc = document.getElementById('btn-desc');
|
const btnDesc = document.getElementById('btn-desc');
|
||||||
const searchInput = document.getElementById('zp-search-input');
|
const searchInput = document.getElementById('zp-search-input');
|
||||||
|
|
||||||
|
// Mode cards: build vs past-paper
|
||||||
|
const modeBuild = document.getElementById('mode-build');
|
||||||
|
const modePast = document.getElementById('mode-past');
|
||||||
|
const pastPanel = document.getElementById('zp-past-panel');
|
||||||
|
const pastField = document.getElementById('past');
|
||||||
|
const shiftField = document.getElementById('shift');
|
||||||
|
const chaptersWrap = document.querySelector('.zp-two-col');
|
||||||
|
let pastMode = false;
|
||||||
|
|
||||||
|
function setMode(past) {{
|
||||||
|
pastMode = past;
|
||||||
|
modeBuild.classList.toggle('active', !past);
|
||||||
|
modePast.classList.toggle('active', past);
|
||||||
|
pastPanel.hidden = !past;
|
||||||
|
// In past mode the chapters list and quota sidebar are meaningless.
|
||||||
|
document.querySelector('.zp-chapters-container').style.display = past ? 'none' : '';
|
||||||
|
document.querySelectorAll('.zp-subject-pills-bar, .zp-search-row, #btn-complete-syllabus, .zp-complete-hint').forEach(el => {{
|
||||||
|
el.style.display = past ? 'none' : '';
|
||||||
|
}});
|
||||||
|
document.getElementById('diff-row').closest('.zp-filter-sec').style.display = past ? 'none' : '';
|
||||||
|
pastField.value = past ? '1' : '';
|
||||||
|
if (past) {{
|
||||||
|
// one flat 180-minute paper, all questions of the year
|
||||||
|
jeeBox.checked = false;
|
||||||
|
jeeBox.dispatchEvent(new Event('change'));
|
||||||
|
countInput.value = 75;
|
||||||
|
}}
|
||||||
|
sync();
|
||||||
|
}}
|
||||||
|
if (modeBuild) modeBuild.addEventListener('click', () => setMode(false));
|
||||||
|
if (modePast) modePast.addEventListener('click', () => setMode(true));
|
||||||
|
|
||||||
|
// Radio pills inside the past panel toggle their .active class
|
||||||
|
document.querySelectorAll('#zp-past-panel input[type=radio]').forEach(r => {{
|
||||||
|
r.addEventListener('change', () => {{
|
||||||
|
r.closest('.zp-pill-row').querySelectorAll('.zp-filter-pill').forEach(p => p.classList.remove('active'));
|
||||||
|
r.closest('.zp-filter-pill').classList.add('active');
|
||||||
|
sync();
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
|
||||||
// 0. Complete syllabus: check every chapter across all subjects
|
// 0. Complete syllabus: check every chapter across all subjects
|
||||||
const completeBtn = document.getElementById('btn-complete-syllabus');
|
const completeBtn = document.getElementById('btn-complete-syllabus');
|
||||||
if (completeBtn) {{
|
if (completeBtn) {{
|
||||||
|
|
@ -778,6 +840,18 @@ pub fn build(ctx: &Ctx, user: &str, blocks: Vec<SubjectBlock>, _form: &BuildForm
|
||||||
|
|
||||||
// 11. Sync state, pill badges, and submit button
|
// 11. Sync state, pill badges, and submit button
|
||||||
function sync() {{
|
function sync() {{
|
||||||
|
if (pastMode) {{
|
||||||
|
const yr = document.querySelector('input[name=past-year]:checked');
|
||||||
|
const sh = document.querySelector('input[name=past-shift]:checked');
|
||||||
|
yearField.value = yr ? yr.value : '';
|
||||||
|
shiftField.value = sh ? sh.value : 'ALL';
|
||||||
|
summaryText.innerHTML = yr
|
||||||
|
? `<b>JEE Main ${{yr.value}}${{sh && sh.value !== 'ALL' ? ' ' + sh.value : ''}}</b><span class="dim">every question of that paper</span>`
|
||||||
|
: '<b>pick a year</b><span class="dim">above</span>';
|
||||||
|
submitBtn.disabled = !yr;
|
||||||
|
btnDesc.textContent = yr ? 'past paper · 180 minutes' : 'pick a year first';
|
||||||
|
return;
|
||||||
|
}}
|
||||||
const checkedChaps = [...document.querySelectorAll('.chap:checked')];
|
const checkedChaps = [...document.querySelectorAll('.chap:checked')];
|
||||||
const checkedSubs = [...document.querySelectorAll('.subtopic:checked')];
|
const checkedSubs = [...document.querySelectorAll('.subtopic:checked')];
|
||||||
chapField.value = checkedChaps.map(c => c.value).join(',');
|
chapField.value = checkedChaps.map(c => c.value).join(',');
|
||||||
|
|
|
||||||
|
|
@ -2797,3 +2797,25 @@ img.brand-logo {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Past-paper picker panel */
|
||||||
|
.zp-past-panel {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-past-panel .zp-filter-heading {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-past-panel .zp-filter-pill input[type=radio] {
|
||||||
|
accent-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue