This commit is contained in:
parent
516219513a
commit
2f26dbb1e7
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
</article>
|
||||
|
||||
{% if leaderboard %}
|
||||
<section class="quiz-leaderboard" aria-label="Quiz leaderboard">
|
||||
<section id="leaderboard" class="quiz-leaderboard" aria-label="Quiz leaderboard">
|
||||
<h2>Leaderboard</h2>
|
||||
<ol class="quiz-leaderboard-list">
|
||||
{% for entry in leaderboard %}
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<form method="POST" action="{{ quiz.url }}/attempts" class="inline-form">
|
||||
<button type="submit" class="btn btn-primary">Play again</button>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user