Update
Some checks failed
DevPlace CI / test (push) Failing after 1h42m39s

This commit is contained in:
retoor 2026-08-20 00:38:07 +02:00
parent 516219513a
commit 2f26dbb1e7
6 changed files with 92 additions and 7 deletions

View File

@ -189,7 +189,9 @@ async def clippy_proxy(request: Request):
"Content-Type": "application/json", "Content-Type": "application/json",
"X-App-Reference": "devplace-devii-v-1-0-0", "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']}" headers["Authorization"] = f"Bearer {cfg['devii_ai_key']}"
async with stealth.stealth_async_client(timeout=45.0) as client: async with stealth.stealth_async_client(timeout=45.0) as client:
upstream = await client.post(cfg["devii_ai_url"], content=body, headers=headers) upstream = await client.post(cfg["devii_ai_url"], content=body, headers=headers)

View File

@ -681,6 +681,20 @@
margin-bottom: var(--space-xl); 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 { .quiz-question-answered .quiz-answer-form {
opacity: 0.7; opacity: 0.7;
} }

View File

@ -16,7 +16,9 @@ export class AppQuizPlayer extends Component {
this._progress = this.querySelector("[data-quiz-progress]"); this._progress = this.querySelector("[data-quiz-progress]");
this._score = this.querySelector("[data-quiz-score]"); this._score = this.querySelector("[data-quiz-score]");
this._timer = this.querySelector("[data-quiz-timer]"); this._timer = this.querySelector("[data-quiz-timer]");
this._slides = Array.from(this.querySelectorAll("[data-quiz-slide]"));
this._bindForms(); this._bindForms();
if (this._slides.length) this._initSlides();
if (this._hasLimit && this._status === "in_progress") this._startTimer(); if (this._hasLimit && this._status === "in_progress") this._startTimer();
} }
@ -24,6 +26,46 @@ export class AppQuizPlayer extends Component {
if (this._interval) clearInterval(this._interval); 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() { _bindForms() {
this.querySelectorAll("[data-quiz-answer-form]").forEach((form) => { this.querySelectorAll("[data-quiz-answer-form]").forEach((form) => {
form.addEventListener("submit", (event) => { form.addEventListener("submit", (event) => {
@ -133,9 +175,9 @@ export class AppQuizPlayer extends Component {
_advance(form) { _advance(form) {
const question = form.closest("[data-quiz-question]"); const question = form.closest("[data-quiz-question]");
if (!question) return; if (!question || !this._slides.length) return;
const next = question.nextElementSibling; const index = this._slides.indexOf(question);
if (next && next.scrollIntoView) next.scrollIntoView({ block: "start", behavior: "smooth" }); if (index !== -1 && index < this._slides.length - 1) this._showSlide(index + 1);
} }
} }

View File

@ -69,7 +69,7 @@
</article> </article>
{% if leaderboard %} {% if leaderboard %}
<section class="quiz-leaderboard" aria-label="Quiz leaderboard"> <section id="leaderboard" class="quiz-leaderboard" aria-label="Quiz leaderboard">
<h2>Leaderboard</h2> <h2>Leaderboard</h2>
<ol class="quiz-leaderboard-list"> <ol class="quiz-leaderboard-list">
{% for entry in leaderboard %} {% for entry in leaderboard %}

View File

@ -46,7 +46,7 @@
<form method="POST" action="{{ quiz.url }}/attempts" class="inline-form"> <form method="POST" action="{{ quiz.url }}/attempts" class="inline-form">
<button type="submit" class="btn btn-primary">Play again</button> <button type="submit" class="btn btn-primary">Play again</button>
</form> </form>
<a href="{{ quiz.url }}/leaderboard" class="btn btn-secondary">Leaderboard</a> <a href="{{ quiz.url }}#leaderboard" class="btn btn-secondary">Leaderboard</a>
<a href="/quizzes" class="btn btn-secondary">All quizzes</a> <a href="/quizzes" class="btn btn-secondary">All quizzes</a>
</div> </div>
</div> </div>

View File

@ -68,6 +68,33 @@ def test_starting_opens_the_player(alice):
page.locator(".quiz-hud").wait_for(state="visible") 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): def test_the_player_renders_a_real_form_per_question(alice):
page, _ = alice page, _ = alice
slug = _author_quiz("Player forms quiz") 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): def test_a_numeric_question_takes_a_typed_answer(alice):
page, _ = alice page, _ = alice
slug = _author_quiz("Player numeric quiz") slug = _author_quiz("Player numeric quiz", questions=(NUMERIC,))
_start(page, slug) _start(page, slug)
question = page.locator(".quiz-question-numeric").first question = page.locator(".quiz-question-numeric").first
question.locator("input[name='answer_text']").fill("1024") question.locator("input[name='answer_text']").fill("1024")