diff --git a/backend/src/main/java/com/odde/doughnut/controllers/RestQuizQuestionController.java b/backend/src/main/java/com/odde/doughnut/controllers/RestQuizQuestionController.java index c2eda10a79..7e9fbfe40a 100644 --- a/backend/src/main/java/com/odde/doughnut/controllers/RestQuizQuestionController.java +++ b/backend/src/main/java/com/odde/doughnut/controllers/RestQuizQuestionController.java @@ -135,6 +135,16 @@ public QuizQuestionAndAnswer addQuestionManually( return quizQuestionService.addQuestion(note, questionAndAnswer); } + @DeleteMapping("/{note}/note-questions/{question}") + @Transactional + public void removeQuestion( + @PathVariable("note") @Schema(type = "integer") Note note, + @PathVariable("question") @Schema(type = "integer") QuizQuestionAndAnswer question) + throws UnexpectedNoAccessRightException { + currentUser.assertAuthorization(note); + quizQuestionService.removeQuestion(note, question); + } + @PostMapping("/{note}/refine-question") @Transactional public QuizQuestionAndAnswer refineQuestion( diff --git a/backend/src/main/java/com/odde/doughnut/services/QuizQuestionService.java b/backend/src/main/java/com/odde/doughnut/services/QuizQuestionService.java index e6cde0e53d..975d147d39 100644 --- a/backend/src/main/java/com/odde/doughnut/services/QuizQuestionService.java +++ b/backend/src/main/java/com/odde/doughnut/services/QuizQuestionService.java @@ -42,6 +42,15 @@ public QuizQuestionAndAnswer addQuestion( return questionAndAnswer; } + public void removeQuestion(Note note, QuizQuestionAndAnswer questionAndAnswer) { + // Approval status should be reset whenever note questions are changed + Notebook parentNotebook = note.getNotebook(); + parentNotebook.setApprovalStatus(ApprovalStatus.NOT_APPROVED); + modelFactoryService.save(parentNotebook); + + modelFactoryService.remove(questionAndAnswer); + } + public QuizQuestionAndAnswer refineQuestion(Note note, QuizQuestionAndAnswer questionAndAnswer) { MCQWithAnswer aiGeneratedRefineQuestion = aiQuestionGenerator.getAiGeneratedRefineQuestion( diff --git a/backend/src/test/java/com/odde/doughnut/controllers/RestQuizQuestionAndAnswerControllerTests.java b/backend/src/test/java/com/odde/doughnut/controllers/RestQuizQuestionAndAnswerControllerTests.java index f22fb41e6f..a19c126963 100644 --- a/backend/src/test/java/com/odde/doughnut/controllers/RestQuizQuestionAndAnswerControllerTests.java +++ b/backend/src/test/java/com/odde/doughnut/controllers/RestQuizQuestionAndAnswerControllerTests.java @@ -517,6 +517,50 @@ void resetNotebookApprovalOnAdd() throws UnexpectedNoAccessRightException { } } + @Nested + class removeAQuestionFromNote { + @Test + void authorization() { + Note note = makeMe.aNote().please(); + QuizQuestionAndAnswer mcqWithAnswer = makeMe.aQuestion().please(); + mcqWithAnswer.setNote(note); + assertThrows( + UnexpectedNoAccessRightException.class, + () -> controller.removeQuestion(note, mcqWithAnswer)); + } + + @Test + void persistent() throws UnexpectedNoAccessRightException { + Note note = makeMe.aNote().creatorAndOwner(currentUser).please(); + QuizQuestionAndAnswer mcqWithAnswer = makeMe.aQuestion().please(); + controller.addQuestionManually(note, mcqWithAnswer); + makeMe.refresh(note); + assertThat(note.getQuizQuestionAndAnswers(), hasSize(1)); + controller.removeQuestion(note, mcqWithAnswer); + makeMe.refresh(note); + assertThat(note.getQuizQuestionAndAnswers(), empty()); + } + + @Test + void resetNotebookApprovalOnRemove() throws UnexpectedNoAccessRightException { + Note note = + makeMe + .aNote() + .creatorAndOwner(currentUser) + .asHeadNoteOfANotebook(currentUser.getEntity().getOwnership()) + .withApprovalStatus(ApprovalStatus.PENDING) + .please(); + QuizQuestionAndAnswer mcqWithAnswer = makeMe.aQuestion().please(); + Notebook parentNotebook = note.getNotebook(); + makeMe.refresh(parentNotebook); + assertEquals(ApprovalStatus.PENDING, parentNotebook.getApprovalStatus()); + + controller.removeQuestion(note, mcqWithAnswer); + makeMe.refresh(note); + assertEquals(ApprovalStatus.NOT_APPROVED, note.getNotebook().getApprovalStatus()); + } + } + @Nested class RefineQuestion { @Test diff --git a/e2e_test/features/assessment/quiz_questions_management.feature b/e2e_test/features/assessment/quiz_questions_management.feature index 1a97d88685..e662a7d20e 100644 --- a/e2e_test/features/assessment/quiz_questions_management.feature +++ b/e2e_test/features/assessment/quiz_questions_management.feature @@ -28,6 +28,20 @@ Feature: Quiz Question Management | Why do cows have hooves instead of feet? | Because they lactose! | Woof! | What? | 0 | Then I can request approval for the notebook "The cow joke" + Scenario: Can delete question + Given I am logged in as an admin + And I have a notebook with the head note "The cow joke" + And I add the following question for the note "The cow joke": + | Stem | Choice 0 | Choice 1 | Choice 2 | Correct Choice Index | + | Why do cows have hooves instead of feet? | Because they lactose! | Woof! | What? | 0 | + And I add the following question for the note "The cow joke": + | Stem | Choice 0 | Choice 1 | Choice 2 | Correct Choice Index | + | What do you call a cow with not leg? | Ground beef | Cowboy | Oxford | 0 | + When I click on the delete button for question "Why do cows have hooves instead of feet?" for note "The cow joke" + Then I should see the questions in the question list of the note "The cow joke": + | Question | Correct Choice | + | What do you call a cow with not leg? | Ground beef | + @usingMockedOpenAiService Scenario: Can generate the question by AI Given OpenAI now generates this question: diff --git a/e2e_test/start/pageObjects/notePage.ts b/e2e_test/start/pageObjects/notePage.ts index bc748e0301..836e6da5c3 100644 --- a/e2e_test/start/pageObjects/notePage.ts +++ b/e2e_test/start/pageObjects/notePage.ts @@ -193,6 +193,10 @@ export const assumeNotePage = (noteTopic?: string) => { addQuestion(row: Record) { this.openQuestionList().addQuestionPage().addQuestion(row) }, + deleteQuestion(question: string) { + this.openQuestionList() + cy.findByText(question).parent('tr').findByText('delete').click() + }, refineQuestion(row: Record) { this.openQuestionList().addQuestionPage().refineQuestion(row) }, diff --git a/e2e_test/step_definitions/note.ts b/e2e_test/step_definitions/note.ts index 057f71ceba..37026ff802 100644 --- a/e2e_test/step_definitions/note.ts +++ b/e2e_test/step_definitions/note.ts @@ -96,6 +96,13 @@ Given( } ) +When( + 'I click on the delete button for question {string} for note {string}', + (question: string, noteTopic: string) => { + start.jumpToNotePage(noteTopic).deleteQuestion(question) + } +) + Given( 'I refine the following question for the note {string}:', (noteTopic: string, data: DataTable) => { diff --git a/frontend/src/components/notes/Questions.vue b/frontend/src/components/notes/Questions.vue index e6cab5710c..db75750de3 100644 --- a/frontend/src/components/notes/Questions.vue +++ b/frontend/src/components/notes/Questions.vue @@ -15,6 +15,7 @@ + @@ -28,6 +29,9 @@ v-for="(question, outerIndex) in questions" :key="question.quizQuestion.multipleChoicesQuestion.stem" > +
edit Approved Question Text A + +
No questions -
+