diff --git a/README.md b/README.md index dfab616..f3f2693 100644 --- a/README.md +++ b/README.md @@ -170,18 +170,22 @@ This software is licensed under AGPL-3.0 or later, at your convenience. - stats - when all values are 0, it doesn't show nicely - features + - add other user to owner of course - show stats for 7 / 14 / 31 days - get all quizzes from a github repo - enter the name of a github / gitlab - compatibility with Delibay - probably difficult - add "delete" button to clean students - modes - - exam mode: only once the admin switch is flicked will the students see if they answered correctly - - live mode: after every question, students see if they answered correctly - - updatable: students can update their questions + - groups of students for race # CHANGELOG +2024-05-23: +- regroup wrong answers in regexps +- when changing quiz for dojo, got the previous quiz in the dojo multiple times +- twice quiz name in corrections + 2024-05-19: - add statistics - show statistics diff --git a/frontend/src/app/components/quiz/quiz.component.html b/frontend/src/app/components/quiz/quiz.component.html index 9d86f25..1990edc 100644 --- a/frontend/src/app/components/quiz/quiz.component.html +++ b/frontend/src/app/components/quiz/quiz.component.html @@ -46,7 +46,7 @@

Answer + [disabled]="corrections" id="quizInput">
@for (match of answer.question.options.regexp!.match; track $index ) { diff --git a/frontend/src/app/course/corrections/corrections.component.html b/frontend/src/app/course/corrections/corrections.component.html index 870c92e..23cbedd 100644 --- a/frontend/src/app/course/corrections/corrections.component.html +++ b/frontend/src/app/course/corrections/corrections.component.html @@ -1,5 +1,4 @@
-

{{quiz.title}}

No results available

@@ -68,12 +67,13 @@

style="margin: auto 0 auto 1em; font-size: 120%;">{{results.chosen[sorted[qIndex][0]][$index]}}
} - @for (wrong of results.texts[sorted[qIndex][0]]; track $index) { + @for (wrong of wrongRegexps; track $index) {
- - + + - 1 + {{ wrong[1] }}
} diff --git a/frontend/src/app/course/corrections/corrections.component.ts b/frontend/src/app/course/corrections/corrections.component.ts index d492a88..a974c59 100644 --- a/frontend/src/app/course/corrections/corrections.component.ts +++ b/frontend/src/app/course/corrections/corrections.component.ts @@ -36,6 +36,7 @@ export class CorrectionsComponent { results!: ResultsSummary; qIndex = 0; sorted: number[][] = []; + wrongRegexps: [string, number][] = []; constructor(private livequiz: LivequizStorageService, private storage: StorageService, private bcs: BreadcrumbService, private stats: StatsService) { @@ -92,22 +93,31 @@ export class CorrectionsComponent { updateClasses() { for (let question = 0; question < this.quiz.questions.length; question++) { const score = Math.round(this.sorted[question][1] * 8); + console.log(question, score); this.tileClasses[question] = "questionTile" + (question % 2 === 1 ? " questionTileOdd" : "") + ` questionTileColor${score}`; } this.tileClasses[this.qIndex] += " questionTileChosen"; + this.wrongRegexps = []; const question = this.quiz.questions[this.sorted[this.qIndex][0]]; if (question.options.multi !== undefined) { this.resultClasses = question.options.multi.correct.map((_) => 'resultCorrect') .concat(question.options.multi!.wrong.map((_) => 'resultWrong')); } else { - this.resultClasses = question.options.regexp!.match.map((_) => 'resultCorrect') + this.resultClasses = question.options.regexp!.match.map((_) => 'resultCorrect'); + const wrongAnswers = this.results.texts[this.sorted[this.qIndex][0]]; + [...new Set(wrongAnswers)].map((wrong, i) => + this.wrongRegexps[i] = [wrong, wrongAnswers.filter((ans) => ans === wrong).length]); } + if (this.results.chosen.length > 0) { this.resultWidth = this.results.chosen[this.sorted[this.qIndex][0]] .map((s) => `${Math.round(s / this.users.length * 50) + 50}%`); + if (this.wrongRegexps.length > 0) { + this.resultWidth = this.resultWidth.concat(this.wrongRegexps.map(([_, n]) => `${Math.round(n / this.users.length * 50) + 50}%`)); + } } } diff --git a/frontend/src/app/course/course-manage/course-manage.component.ts b/frontend/src/app/course/course-manage/course-manage.component.ts index 5932b7c..8eab3fa 100644 --- a/frontend/src/app/course/course-manage/course-manage.component.ts +++ b/frontend/src/app/course/course-manage/course-manage.component.ts @@ -33,14 +33,7 @@ export class CourseManageComponent { private stats: StatsService) { } async ngOnChanges() { - this.quizzes = []; - for (const id of this.course.quizIds) { - this.quizzes.push(await this.storage.getNomad(id, new Quiz())); - } - if (this.course.state.state !== CourseStateEnum.Idle) { - const dojo = await this.livequiz.getDojo(this.course.state.getDojoID()); - this.quiz = await this.livequiz.getQuiz(dojo.quizId); - } + await this.updateQuizzes(); if (!this.isStudent()) { this.user.courses.set(this.course.id.toHex(), this.course.name); this.stats.add(StatsService.course_join); @@ -49,6 +42,21 @@ export class CourseManageComponent { this.user.addCourse(this.course); } this.user.update(); + await this.updateDojo(); + } + + async updateDojo(){ + if (this.course.state.state !== CourseStateEnum.Idle) { + const dojo = await this.livequiz.getDojo(this.course.state.getDojoID()); + this.quiz = await this.livequiz.getQuiz(dojo.quizId); + } + } + + async updateQuizzes(){ + this.quizzes = []; + for (const id of this.course.quizIds) { + this.quizzes.push(await this.storage.getNomad(id, new Quiz())); + } } isAdmin(): boolean { @@ -62,6 +70,7 @@ export class CourseManageComponent { async dojoQuiz(id: QuizID) { this.stats.add(StatsService.dojo_start); await this.livequiz.setDojoQuiz(this.course.id, id); + await this.updateDojo(); } async dojoCorrections() { @@ -71,6 +80,7 @@ export class CourseManageComponent { async dojoIdle() { this.stats.add(StatsService.dojo_stop); await this.livequiz.setDojoIdle(this.course.id); + await this.updateDojo(); } async deleteQuiz(id: QuizID) { @@ -83,7 +93,7 @@ export class CourseManageComponent { this.course.state.state = CourseStateEnum.Idle; } this.stats.add(StatsService.quiz_delete); - this.ngOnChanges(); + await this.updateDojo(); } } @@ -103,7 +113,7 @@ export class CourseManageComponent { this.course.quizIds.push(q.id); this.stats.add(StatsService.quiz_create_upload); } - this.ngOnChanges(); + this.updateQuizzes(); } catch (e) { await ModalModule.openOKCancel(this.dialog, 'Error', `While reading quiz: ${e}` diff --git a/frontend/src/test/livequiz.ts b/frontend/src/test/livequiz.ts index b5edb7d..4654eca 100644 --- a/frontend/src/test/livequiz.ts +++ b/frontend/src/test/livequiz.ts @@ -38,12 +38,18 @@ export class Livequiz { return this.by(By.xpath(xp)); } - text(t: string): WEP { - return this.xpath(`//*[contains(text(), "${t}")]`); + text(t: string, occurence = 1): WEP { + return this.xpath(`(//*[contains(text(), "${t}")])[${occurence}]`); } - async click(t: string) { - await this.text(t).click(); + async click(...texts: string[]) { + for (const text of texts) { + await this.text(text).click(); + } + } + + async find(t: string){ + await this.text(t).find(); } } export class WEP { diff --git a/frontend/src/test/newUser.spec.ts b/frontend/src/test/newUser.spec.ts index bbc679b..8fd360c 100644 --- a/frontend/src/test/newUser.spec.ts +++ b/frontend/src/test/newUser.spec.ts @@ -1,7 +1,7 @@ import { Livequiz } from './livequiz'; import { readFileSync } from 'fs'; -describe("Logging in", () => { +describe("E2E tests", () => { it("Correctly identifies 2 users", async () => { const admin = await Livequiz.reset(); await admin.id('cname').sendKeys('Testing'); @@ -22,6 +22,7 @@ describe("Logging in", () => { await user1.id("userName").sendKeys("user1"); let user2 = await Livequiz.init(courseUrl); + await Livequiz.wait(200); await user2.id("userName").clear(); await user2.id("userName").sendKeys("user2"); await user2.click("LiveQuiz"); @@ -51,7 +52,7 @@ describe("Logging in", () => { await Livequiz.wait(100); admin.browser.quit(); - }); + }, 10000); it("Stores stats", async () => { const admin = await Livequiz.reset(); @@ -59,14 +60,86 @@ describe("Logging in", () => { await Livequiz.wait(200); let user1 = await Livequiz.init(); await user1.id('cname').sendKeys('Testing'); - await user1.css('button').click(); - await user1.click('Testing'); - await user1.click('Create Quiz'); - await user1.click('Save'); - await user1.click('Start Quiz'); - await user1.click('Enter Dojo'); + await user1.click('Add a course', 'Testing', 'Create Quiz', 'Save', 'Start Quiz', 'Enter Dojo'); await Livequiz.wait(200); await admin.click("Stats"); - }) -}) + + user1.browser.quit(); + admin.browser.quit(); + }); + + it("Corrections", async () => { + // Verify that the fully-wrong answer comes before the half-correct, which comes + // before the fully-correct one. + const admin = await Livequiz.reset(); + await admin.id('cname').sendKeys('Testing'); + await admin.click('Add a course', 'Testing'); + await admin.id('fileInput').sendKeys(`${__dirname}/quiz1.md`); + await admin.click('Start Quiz', 'Enter Dojo', 'wrong', 'Next', 'correct', 'Next'); + await admin.id('quizInput').sendKeys('one'); + await admin.click('Testing', 'Start Corrections'); + + await admin.find("Question 1"); + await admin.click('Next'); + await admin.find("Question 2"); + await admin.click('Next'); + await admin.find("Question 3"); + admin.browser.quit(); + }); + + it("Change dojo", async () => { + // Create two quizzes and change the quiz in the dojo + const admin = await Livequiz.reset(); + await admin.id('cname').sendKeys('Testing'); + await admin.click('Add a course', 'Testing'); + await admin.id('fileInput').sendKeys(`${__dirname}/quiz1.md`); + await admin.click('Create Quiz', 'Save'); + await admin.click('Start Quiz'); + await admin.find('Quiz in Dojo: Test Quiz'); + await admin.text('Start Quiz', 2).click(); + await admin.find('Quiz in Dojo: Title of Quiz'); + admin.browser.quit(); + }); + + it("Sorts wrong regexps", async () => { + // Create two quizzes and change the quiz in the dojo + const admin = await Livequiz.reset(); + await admin.id('cname').sendKeys('Testing'); + await admin.click('Add a course', 'Testing'); + await admin.id('fileInput').sendKeys(`${__dirname}/quiz1.md`); + await admin.click('Start Quiz'); + const courseUrl = await admin.browser.getCurrentUrl() + "/dojo"; + + const user1 = await Livequiz.init(courseUrl); + const user2 = await Livequiz.init(courseUrl); + const user3 = await Livequiz.init(courseUrl); + await Livequiz.wait(200); + await user1.click('Next', 'Next'); + await user1.id('quizInput').sendKeys(' three!!'); + await user2.click('Next', 'Next'); + await user2.id('quizInput').sendKeys('four'); + await user3.click('Next', 'Next'); + await user3.id('quizInput').sendKeys('three!!'); + + await Livequiz.wait(300); + await admin.click("Corrections", "Next", "Next"); + await admin.text('three').find(); + await throwError(() => admin.text('three!!', 2).find()); + await admin.text('four').find(); + + await admin.browser.quit(); + await user1.browser.quit(); + await user2.browser.quit(); + await user3.browser.quit(); + }, 10000) +}); + +async function throwError(f: () => Promise): Promise { + try { + await f(); + } catch (_) { + return; + } + return Promise.reject("Didn't throw error"); +}