From 2f26dbb1e7934f8951474af48a43267a05265540 Mon Sep 17 00:00:00 2001 From: retoor Date: Thu, 20 Aug 2026 00:38:07 +0200 Subject: [PATCH] Update --- devplacepy/routers/devii.py | 4 +- devplacepy/static/css/quiz.css | 14 ++++++ .../static/js/components/AppQuizPlayer.js | 48 +++++++++++++++++-- devplacepy/templates/quiz.html | 2 +- devplacepy/templates/quiz_results.html | 2 +- tests/e2e/quizzes/play.py | 29 ++++++++++- 6 files changed, 92 insertions(+), 7 deletions(-) diff --git a/devplacepy/routers/devii.py b/devplacepy/routers/devii.py index 1ede21df..1033427b 100644 --- a/devplacepy/routers/devii.py +++ b/devplacepy/routers/devii.py @@ -189,7 +189,9 @@ async def clippy_proxy(request: Request): "Content-Type": "application/json", "X-App-Reference": "devplace-devii-v-1-0-0", } - if cfg.get("devii_ai_key"): + if user.get("api_key"): + headers["Authorization"] = f"Bearer {user['api_key']}" + elif cfg.get("devii_ai_key"): headers["Authorization"] = f"Bearer {cfg['devii_ai_key']}" async with stealth.stealth_async_client(timeout=45.0) as client: upstream = await client.post(cfg["devii_ai_url"], content=body, headers=headers) diff --git a/devplacepy/static/css/quiz.css b/devplacepy/static/css/quiz.css index 2896143b..70a8385b 100644 --- a/devplacepy/static/css/quiz.css +++ b/devplacepy/static/css/quiz.css @@ -681,6 +681,20 @@ margin-bottom: var(--space-xl); } +.quiz-slide-nav { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-md); + margin-bottom: var(--space-lg); +} + +.quiz-slide-counter { + color: var(--text-secondary); + font-size: 0.86rem; + font-variant-numeric: tabular-nums; +} + .quiz-question-answered .quiz-answer-form { opacity: 0.7; } diff --git a/devplacepy/static/js/components/AppQuizPlayer.js b/devplacepy/static/js/components/AppQuizPlayer.js index 17b1e70b..d2cb2845 100644 --- a/devplacepy/static/js/components/AppQuizPlayer.js +++ b/devplacepy/static/js/components/AppQuizPlayer.js @@ -16,7 +16,9 @@ export class AppQuizPlayer extends Component { this._progress = this.querySelector("[data-quiz-progress]"); this._score = this.querySelector("[data-quiz-score]"); this._timer = this.querySelector("[data-quiz-timer]"); + this._slides = Array.from(this.querySelectorAll("[data-quiz-slide]")); this._bindForms(); + if (this._slides.length) this._initSlides(); if (this._hasLimit && this._status === "in_progress") this._startTimer(); } @@ -24,6 +26,46 @@ export class AppQuizPlayer extends Component { if (this._interval) clearInterval(this._interval); } + _initSlides() { + const startAt = this._slides.findIndex((slide) => !slide.classList.contains("quiz-question-answered")); + this._buildSlideNav(); + this._showSlide(startAt === -1 ? this._slides.length - 1 : startAt); + } + + _buildSlideNav() { + const host = this.querySelector("[data-quiz-slides]"); + if (!host) return; + const nav = document.createElement("div"); + nav.className = "quiz-slide-nav"; + this._prevBtn = document.createElement("button"); + this._prevBtn.type = "button"; + this._prevBtn.className = "btn btn-secondary"; + this._prevBtn.textContent = "Previous"; + this._prevBtn.addEventListener("click", () => this._showSlide(this._current - 1)); + this._counter = document.createElement("span"); + this._counter.className = "quiz-slide-counter"; + this._nextBtn = document.createElement("button"); + this._nextBtn.type = "button"; + this._nextBtn.className = "btn btn-secondary"; + this._nextBtn.textContent = "Next"; + this._nextBtn.addEventListener("click", () => this._showSlide(this._current + 1)); + nav.appendChild(this._prevBtn); + nav.appendChild(this._counter); + nav.appendChild(this._nextBtn); + host.before(nav); + } + + _showSlide(index) { + if (!this._slides.length) return; + this._current = Math.max(0, Math.min(this._slides.length - 1, index)); + this._slides.forEach((slide, i) => { + slide.hidden = i !== this._current; + }); + if (this._prevBtn) this._prevBtn.disabled = this._current === 0; + if (this._nextBtn) this._nextBtn.disabled = this._current === this._slides.length - 1; + if (this._counter) this._counter.textContent = `Question ${this._current + 1} of ${this._slides.length}`; + } + _bindForms() { this.querySelectorAll("[data-quiz-answer-form]").forEach((form) => { form.addEventListener("submit", (event) => { @@ -133,9 +175,9 @@ export class AppQuizPlayer extends Component { _advance(form) { const question = form.closest("[data-quiz-question]"); - if (!question) return; - const next = question.nextElementSibling; - if (next && next.scrollIntoView) next.scrollIntoView({ block: "start", behavior: "smooth" }); + if (!question || !this._slides.length) return; + const index = this._slides.indexOf(question); + if (index !== -1 && index < this._slides.length - 1) this._showSlide(index + 1); } } diff --git a/devplacepy/templates/quiz.html b/devplacepy/templates/quiz.html index 637f1920..1a989b4a 100644 --- a/devplacepy/templates/quiz.html +++ b/devplacepy/templates/quiz.html @@ -69,7 +69,7 @@ {% if leaderboard %} -
+

Leaderboard

    {% for entry in leaderboard %} diff --git a/devplacepy/templates/quiz_results.html b/devplacepy/templates/quiz_results.html index cb145302..2e917f2c 100644 --- a/devplacepy/templates/quiz_results.html +++ b/devplacepy/templates/quiz_results.html @@ -46,7 +46,7 @@
    - Leaderboard + Leaderboard All quizzes diff --git a/tests/e2e/quizzes/play.py b/tests/e2e/quizzes/play.py index e6e59213..4533123b 100644 --- a/tests/e2e/quizzes/play.py +++ b/tests/e2e/quizzes/play.py @@ -68,6 +68,33 @@ def test_starting_opens_the_player(alice): page.locator(".quiz-hud").wait_for(state="visible") +def test_the_player_shows_one_question_at_a_time(alice): + page, _ = alice + slug = _author_quiz("Player one at a time quiz") + _start(page, slug) + questions = page.locator(".quiz-question") + assert questions.nth(0).is_visible() + assert not questions.nth(1).is_visible() + page.locator(".quiz-slide-nav button:has-text('Next')").click() + assert not questions.nth(0).is_visible() + assert questions.nth(1).is_visible() + page.locator(".quiz-slide-nav button:has-text('Previous')").click() + assert questions.nth(0).is_visible() + assert not questions.nth(1).is_visible() + + +def test_answering_advances_to_the_next_question(alice): + page, _ = alice + slug = _author_quiz("Player advance quiz") + _start(page, slug) + questions = page.locator(".quiz-question") + questions.nth(0).locator("input[type='radio']").last.check() + questions.nth(0).locator("button:has-text('Submit answer')").click() + questions.nth(0).locator(".quiz-grade").wait_for(state="visible") + questions.nth(1).wait_for(state="visible") + assert not questions.nth(0).is_visible() + + def test_the_player_renders_a_real_form_per_question(alice): page, _ = alice slug = _author_quiz("Player forms quiz") @@ -129,7 +156,7 @@ def test_an_answered_question_cannot_be_resubmitted(alice): def test_a_numeric_question_takes_a_typed_answer(alice): page, _ = alice - slug = _author_quiz("Player numeric quiz") + slug = _author_quiz("Player numeric quiz", questions=(NUMERIC,)) _start(page, slug) question = page.locator(".quiz-question-numeric").first question.locator("input[name='answer_text']").fill("1024")