Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add remove functionality for questions - Tim #1016

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions e2e_test/features/assessment/quiz_questions_management.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions e2e_test/start/pageObjects/notePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ export const assumeNotePage = (noteTopic?: string) => {
addQuestion(row: Record<string, string>) {
this.openQuestionList().addQuestionPage().addQuestion(row)
},
deleteQuestion(question: string) {
this.openQuestionList()
cy.findByText(question).parent('tr').findByText('delete').click()
},
refineQuestion(row: Record<string, string>) {
this.openQuestionList().addQuestionPage().refineQuestion(row)
},
Expand Down
7 changes: 7 additions & 0 deletions e2e_test/step_definitions/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
24 changes: 22 additions & 2 deletions frontend/src/components/notes/Questions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<table class="question-table mt-2" v-if="questions.length">
<thead>
<tr>
<th>edit</th>
<th>Approved</th>
<th>Question Text</th>
<th>A</th>
Expand All @@ -28,6 +29,9 @@
v-for="(question, outerIndex) in questions"
:key="question.quizQuestion.multipleChoicesQuestion.stem"
>
<td>
<button @click="removeQuestion(question.id)">delete</button>
</td>
<td>
<input
:id="'checkbox-' + outerIndex"
Expand Down Expand Up @@ -56,42 +60,58 @@
</table>
<div v-else class="no-questions">
<b >No questions</b>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { PropType, onMounted, ref } from "vue"
import { onMounted, PropType, ref } from "vue"
import { Note, QuizQuestionAndAnswer } from "@/generated/backend"
import useLoadingApi from "@/managedApi/useLoadingApi"
import NoteAddQuestion from "./NoteAddQuestion.vue"
import PopButton from "../commons/Popups/PopButton.vue"

const { managedApi } = useLoadingApi()

const props = defineProps({
note: {
type: Object as PropType<Note>,
required: true,
},
})

const questions = ref<QuizQuestionAndAnswer[]>([])

const fetchQuestions = async () => {
questions.value =
await managedApi.restQuizQuestionController.getAllQuestionByNote(
props.note.id
)
}

const questionAdded = (newQuestion: QuizQuestionAndAnswer) => {
if (newQuestion == null) {
return
}
questions.value.push(newQuestion)
}

const toggleApproval = async (questionId?: number) => {
if (questionId) {
await managedApi.restQuizQuestionController.toggleApproval(questionId)
}
}

const removeQuestion = (questionId: number) => {
managedApi.restQuizQuestionController
.removeQuestion(props.note.id, questionId)
.then(() => {
questions.value = questions.value.filter((question) => {
return question.id !== questionId
})
})
}

onMounted(() => {
fetchQuestions()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,26 @@ export class RestQuizQuestionControllerService {
},
});
}
/**
* @param note
* @param question
* @returns any OK
* @throws ApiError
*/
public removeQuestion(
note: number,
question: number,
): CancelablePromise<any> {
return this.httpRequest.request({
method: 'DELETE',
url: '/api/quiz-questions/{note}/note-questions/{question}',
path: {
'note': note,
'question': question,
},
errors: {
500: `Internal Server Error`,
},
});
}
}
41 changes: 41 additions & 0 deletions frontend/tests/components/notes/Questions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Note } from "@/generated/backend"
import makeMe from "../../fixtures/makeMe"
import helper from "../../helpers"
import Questions from "@/components/notes/Questions.vue"
import { beforeEach, expect } from "vitest"
import { flushPromises } from "@vue/test-utils"

describe("Questions.spec", () => {
const note: Note = {
...makeMe.aNote.please(),
}
let wrapper
const deleteCall = vi.fn().mockResolvedValue({})

beforeEach(() => {
const questions = [
makeMe.aQuizQuestionAndAnswer.please(),
makeMe.aQuizQuestionAndAnswer.please(),
makeMe.aQuizQuestionAndAnswer.please(),
]
helper.managedApi.restQuizQuestionController.getAllQuestionByNote = vi
.fn()
.mockResolvedValue(questions)
helper.managedApi.restQuizQuestionController.removeQuestion = deleteCall
wrapper = helper.component(Questions).withProps({ note }).mount()
})

it("Renders list of questions", async () => {
expect(
(wrapper.find(".question-table").element as HTMLTableElement).rows.length
).toBe(4)
})

it("Deletes the question", async () => {
// wrapper.find(".question-table > tr > button").trigger("click")
expect(wrapper.findAll(".question-table tbody tr").length).toBe(3)
wrapper.findAll(".question-table tbody tr td button")[0].trigger("click")
await flushPromises()
expect(wrapper.findAll(".question-table tbody tr").length).toBe(2)
})
})
25 changes: 25 additions & 0 deletions open_api_docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2711,6 +2711,31 @@ paths:
type: array
items:
type: string
/api/quiz-questions/{note}/note-questions/{question}:
delete:
tags:
- rest-quiz-question-controller
operationId: removeQuestion
parameters:
- name: note
in: path
required: true
schema:
type: integer
- name: question
in: path
required: true
schema:
type: integer
responses:
"500":
description: Internal Server Error
content:
'*/*':
schema:
type: string
"200":
description: OK
components:
schemas:
Circle:
Expand Down
4 changes: 2 additions & 2 deletions setup-doughnut-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ install_nixpkg_manager() {
if ! command -v nix >/dev/null 2>&1; then
download_nixpkg_manager_install_script
touch .bash_profile
if [ "${os_type}" == "Mac" || "${os_type}" == "Linux" ]; then
./install-nix -s -- install
if [ "${os_type}" == "Mac" ] || [ "${os_type}" == "Linux" ]; then
./install-nix install
else
echo "Unsupported OS Platform for Nix development enviroment. Exiting!!!"
exit 1
Expand Down