diff --git a/apps/frontend-manage/src/components/questions/manipulation/options/SelectionOptions.tsx b/apps/frontend-manage/src/components/questions/manipulation/options/SelectionOptions.tsx index eec178f7db..c125941e8b 100644 --- a/apps/frontend-manage/src/components/questions/manipulation/options/SelectionOptions.tsx +++ b/apps/frontend-manage/src/components/questions/manipulation/options/SelectionOptions.tsx @@ -6,6 +6,7 @@ import { FormikSelectField, FormikSwitchField, FormLabel, + UserNotification, } from '@uzh-bf/design-system' import { useField } from 'formik' import { useTranslations } from 'next-intl' @@ -65,7 +66,7 @@ function SelectionOptions({ values }: SelectionOptionsProps) { // udpate the selected correct answers if the answer collection changes useEffect(() => { - if (!field.value) { + if (!field.value || !collectionAnswers || collectionAnswers.length === 0) { return } @@ -82,6 +83,16 @@ function SelectionOptions({ values }: SelectionOptionsProps) { return } + if (collections.length === 0) { + return ( + + ) + } + return (
@@ -96,7 +107,7 @@ function SelectionOptions({ values }: SelectionOptionsProps) { label: collection.name, value: String(collection.id), data: { - cy: `select-answer-collection-${collection.id}`, + cy: `select-answer-collection-${collection.name}`, }, }))} data={{ cy: 'select-answer-collection' }} @@ -135,23 +146,24 @@ function SelectionOptions({ values }: SelectionOptionsProps) { tooltip={t('manage.questionForms.correctAnswerOptionsTooltip')} labelType="small" /> - 'w-full', + }} + onChange={(newValue) => + helpers.setValue(newValue.map((tag) => tag.value)) + } + placeholder={t('manage.questionForms.selectAnswerOptions')} + noOptionsMessage={() => + t('manage.questionForms.noMatchingOptionFound') + } + /> +
) : null} diff --git a/cypress/cypress/e2e/D-questions-workflow.cy.ts b/cypress/cypress/e2e/D-questions-workflow.cy.ts index 8ca2183c16..4802440068 100644 --- a/cypress/cypress/e2e/D-questions-workflow.cy.ts +++ b/cypress/cypress/e2e/D-questions-workflow.cy.ts @@ -65,6 +65,19 @@ const FTSampleSolution = [ 'Sample Solution 3', ] +const SETitle = 'Selection Question Title' +const SEContent = 'Selection Question Text' +const SEExplanation = 'Selection Question Explanation' +const SECollection = 'Private Collection (Vegetables)' // from seed (access otherwise is tested in resources workflow) +const SEInputs = 2 +const SESolutions = ['Cabbage', 'Cucumber'] +const SETitleEdited = 'Selection Question Title Edited' +const SEContentEdited = 'Selection Question Text Edited' +const SEExplanationEdited = 'Selection Question Explanation Edited' +const SECollectionEdited = 'Public Collection (Fruits)' // from seed +const SEInputsEdited = 1 +const SESolutionsEdited = ['Apple', 'Banana'] + describe('Create different types of elements (with and without sample solution) and edit them', () => { beforeEach(() => { cy.loginLecturer() @@ -1054,6 +1067,154 @@ describe('Create different types of elements (with and without sample solution) cy.get('[data-cy="close-question-modal"]').click() }) + it('Create a Selection question', () => { + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]') + .should('exist') + .contains(messages.shared.SC.typeLabel) + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + cy.get('[data-cy="select-question-type"]') + .should('exist') + .contains(messages.shared.SELECTION.typeLabel) + cy.get('[data-cy="save-new-question"]').should('be.disabled') + + cy.get('[data-cy="insert-question-title"]').click().type(SETitle) + cy.get('[data-cy="save-new-question"]').should('be.disabled') + cy.get('[data-cy="select-question-status"]').click() + cy.get( + `[data-cy="select-question-status-${messages.shared.READY.statusLabel}"]` + ).click() + cy.get('[data-cy="insert-question-text"]').realClick().type(SEContent) + cy.get('[data-cy="insert-question-explanation"]') + .realClick() + .type(SEExplanation) + cy.get('[data-cy="save-new-question"]').should('be.disabled') + + cy.get('[data-cy="select-answer-collection"]').contains( + messages.manage.questionForms.selectCollection + ) + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${SECollection}"]`).click() + cy.get('[data-cy="select-answer-collection"]').contains(SECollection) + cy.get('[data-cy="save-new-question"]').should('be.disabled') + cy.get('[data-cy="configure-number-of-inputs"]') + .click() + .type(String(SEInputs)) + cy.get('[data-cy="save-new-question"]').click() + cy.wait(500) + + cy.get(`[data-cy="element-item-${SETitle}"]`).contains(SEContent) + cy.get(`[data-cy="element-item-${SETitle}"]`).contains(SETitle) + cy.get(`[data-cy="element-item-${SETitle}"]`).contains( + messages.shared.READY.statusLabel + ) + + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + // TODO: check that preview of selection question is visible and correct + }) + + it('Verify that the correct content has been saved', () => { + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + cy.get('[data-cy="insert-question-title"]').should('have.value', SETitle) + cy.get('[data-cy="select-question-status"]').contains( + messages.shared.READY.statusLabel + ) + cy.get('[data-cy="insert-question-text"]').realClick().contains(SEContent) + cy.get('[data-cy="insert-question-explanation"]') + .realClick() + .contains(SEExplanation) + cy.get('[data-cy="select-answer-collection"]').contains(SECollection) + cy.get('[data-cy="configure-number-of-inputs"]').should( + 'have.value', + SEInputs + ) + cy.get('[data-cy="close-question-modal"]').click() + }) + + it('Add a sample solution to the created selection question', () => { + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + cy.get('[data-cy="insert-question-title"]').should('have.value', SETitle) + + cy.get('[data-cy="configure-sample-solution"]').click() + cy.get('[data-cy="save-new-question"]').should('be.disabled') // at least one correct answer is required + cy.get('[data-cy="choose-correct-answer-options"]').click() + cy.findByText(SESolutions[0]).realClick() + cy.get('[data-cy="choose-correct-answer-options"]').contains(SESolutions[0]) + cy.get('[data-cy="save-new-question"]').should('be.disabled') // number of solutions needs to be >= number of inputs + SESolutions.slice(1).forEach((solution) => { + cy.get('[data-cy="choose-correct-answer-options"]').click() + cy.findByText(solution).realClick() + cy.get('[data-cy="choose-correct-answer-options"]').contains(solution) + }) + cy.get('[data-cy="save-new-question"]').click() + }) + + it('Verify that the sample solution has been stored correctly for the modified selection question', () => { + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + cy.get('[data-cy="insert-question-title"]').should('have.value', SETitle) + + SESolutions.forEach((solution) => { + cy.get('[data-cy="choose-correct-answer-options"]').contains(solution) + }) + cy.get('[data-cy="close-question-modal"]').click() + }) + + it('Edit the selection question and change the answer collection (including new sample solutions)', () => { + cy.get(`[data-cy="edit-question-${SETitle}"]`).click() + cy.get('[data-cy="insert-question-title"]') + .click() + .clear() + .type(SETitleEdited) + cy.get('[data-cy="insert-question-text"]') + .realClick() + .clear() + .type(SEContentEdited) + cy.get('[data-cy="insert-question-explanation"]') + .realClick() + .clear() + .type(SEExplanationEdited) + + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${SECollectionEdited}"]`).click() + cy.get('[data-cy="select-answer-collection"]').contains(SECollectionEdited) + cy.get('[data-cy="save-new-question"]').should('be.disabled') // answer options are cleared on collection change + cy.get('[data-cy="configure-number-of-inputs"]') + .click() + .clear() + .type(String(SEInputsEdited)) + SESolutionsEdited.forEach((solution) => { + cy.get('[data-cy="choose-correct-answer-options"]').click() + cy.findByText(solution).realClick() + }) + + cy.get('[data-cy="save-new-question"]').click() + }) + + it('Verify that the edited state of the selection question persists', () => { + cy.get(`[data-cy="edit-question-${SETitleEdited}"]`).click() + cy.get('[data-cy="insert-question-title"]').should( + 'have.value', + SETitleEdited + ) + cy.get('[data-cy="insert-question-text"]') + .realClick() + .contains(SEContentEdited) + cy.get('[data-cy="insert-question-explanation"]') + .realClick() + .contains(SEExplanationEdited) + cy.get('[data-cy="select-answer-collection"]').contains(SECollectionEdited) + cy.get('[data-cy="configure-number-of-inputs"]').should( + 'have.value', + SEInputsEdited + ) + SESolutionsEdited.forEach((solution) => { + cy.get('[data-cy="choose-correct-answer-options"]').contains(solution) + }) + }) + it('Create a new question, duplicates it and then deletes the duplicate again', () => { const randomNumber = uuid() const questionTitle = 'A Single Choice ' + randomNumber @@ -1126,5 +1287,9 @@ describe('Create different types of elements (with and without sample solution) cy.get(`[data-cy="delete-question-${FTTitleEdited}"]`).click() cy.get('[data-cy="confirm-question-deletion"]').click() cy.get(`[data-cy="element-item-${FTTitleEdited}"]`).should('not.exist') + + cy.get(`[data-cy="delete-question-${SETitleEdited}"]`).click() + cy.get('[data-cy="confirm-question-deletion"]').click() + cy.get(`[data-cy="element-item-${SETitleEdited}"]`).should('not.exist') }) }) diff --git a/cypress/cypress/e2e/K-resources-workflow.cy.ts b/cypress/cypress/e2e/K-resources-workflow.cy.ts index 0d12740ff0..5959a44a38 100644 --- a/cypress/cypress/e2e/K-resources-workflow.cy.ts +++ b/cypress/cypress/e2e/K-resources-workflow.cy.ts @@ -216,6 +216,24 @@ describe('Create, edit and share answer collections', () => { }) }) + it('Verify that all three answer collections can be used in selection questions by owner', () => { + cy.loginLecturer() + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${publicName}"]`).should('exist') + cy.get(`[data-cy="select-answer-collection-${restrictedName}"]`).should( + 'exist' + ) + cy.get(`[data-cy="select-answer-collection-${privateNameNew}"]`).should( + 'exist' + ) + }) + it('Request access to the restricted answer catalogue for user pro1', () => { cy.loginIndividualCatalyst() cy.get('[data-cy="resources"]').click() @@ -373,6 +391,26 @@ describe('Create, edit and share answer collections', () => { }) }) + it('Verify that only the shared and restricted answer catalogue is available during question creation for user pro1', () => { + cy.loginIndividualCatalyst() + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${publicName}"]`).should( + 'not.exist' + ) + cy.get(`[data-cy="select-answer-collection-${restrictedName}"]`).should( + 'exist' + ) + cy.get(`[data-cy="select-answer-collection-${privateNameNew}"]`).should( + 'not.exist' + ) + }) + it('Verify that user pro2 does not have access to the restricted answer catalogue', () => { cy.loginInstitutionalCatalyst() cy.get('[data-cy="resources"]').click() @@ -381,6 +419,18 @@ describe('Create, edit and share answer collections', () => { ) }) + it('Verify that no answer catalogue is available for user pro2', () => { + cy.loginInstitutionalCatalyst() + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + + cy.get('[data-cy="select-answer-collection"]').should('not.exist') + cy.findByText(messages.manage.questionForms.SEAnswerCollectionRequired) + }) + it('Import the public answer catalogue for user pro1 and verify access to it', () => { cy.loginIndividualCatalyst() cy.get('[data-cy="resources"]').click() @@ -430,6 +480,24 @@ describe('Create, edit and share answer collections', () => { cy.get('[data-cy="close-viewing-collection-modal"]').click() }) + it('Verify that imported public answer collection is also available for during question creation user pro1', () => { + cy.loginIndividualCatalyst() + cy.get('[data-cy="create-question"]').click() + cy.get('[data-cy="select-question-type"]').click() + cy.get( + `[data-cy="select-question-type-${messages.shared.SELECTION.typeLabel}"]` + ).click() + + cy.get('[data-cy="select-answer-collection"]').click() + cy.get(`[data-cy="select-answer-collection-${publicName}"]`).should('exist') + cy.get(`[data-cy="select-answer-collection-${restrictedName}"]`).should( + 'exist' + ) + cy.get(`[data-cy="select-answer-collection-${privateNameNew}"]`).should( + 'not.exist' + ) + }) + it('Login again as user pro1 and verify that the answer catalogues are still visible', () => { cy.loginIndividualCatalyst() cy.get('[data-cy="resources"]').click() diff --git a/packages/i18n/messages/de.ts b/packages/i18n/messages/de.ts index 18d6bced0d..82707a9b36 100644 --- a/packages/i18n/messages/de.ts +++ b/packages/i18n/messages/de.ts @@ -994,6 +994,8 @@ Da die KlickerUZH-App noch nicht im iOS-App-Store verfügbar ist, folgen Sie die 'Nehmen Sie hier optionale Einstellungen für die numerische Frage vor. Bitte beachten Sie, dass der Antwortbereich von numerischen Fragen auf das Intervall [-1e30,1e30] begrenzt ist. Sollten Sie grössere Zahlen benötigen, verwenden Sie bitte eine Freitext-Frage.', SELECTIONOptionsTooltip: 'Wählen Sie hier die Antwort-Sammlung aus welcher die Studierenden die korrekten Antworten auswählen sollen.', + SEAnswerCollectionRequired: + "Zur Erstellung von Auswahl-Fragen benötigen Sie Zugriff auf mindestens eine Antwort-Sammlung!. Sie können diese entweder unter dem Reiter 'Ressourcen' selbst erstellen oder dort eine bestehende Sammlung anderer Nutzer importieren.", selectCollection: 'Sammlung auswählen...', answerCollection: 'Antwort-Sammlung', numberOfInputs: 'Anzahl Eingabefelder', diff --git a/packages/i18n/messages/en.ts b/packages/i18n/messages/en.ts index 996e09af82..82e95175be 100644 --- a/packages/i18n/messages/en.ts +++ b/packages/i18n/messages/en.ts @@ -995,6 +995,8 @@ Since the KlickerUZH app is not yet available on the iOS App Store, follow these 'Enter optional settings for the numerical question here. Please note that the range of numbers for numerical questions is limited to the interval [-1e30,1e30] for technical reasons. Should you require to use larger numbers, please use a free text question instead.', SELECTIONOptionsTooltip: 'Please select the answer collection from which the students should select the correct answers.', + SEAnswerCollectionRequired: + 'To create selection questions, you need access to at least one answer collection! You can either create one yourself under the "Resources" tab or import an existing collection from other users there.', answerCollection: 'Answer collection', selectCollection: 'Select collection...', numberOfInputs: 'Number of inputs', diff --git a/packages/prisma/src/data/data/TEST.ts b/packages/prisma/src/data/data/TEST.ts index 9659445980..05329cce11 100644 --- a/packages/prisma/src/data/data/TEST.ts +++ b/packages/prisma/src/data/data/TEST.ts @@ -539,6 +539,12 @@ export const ANSWER_COLLECTIONS = [ { value: 'Dill', }, + { + value: 'Cucumber', + }, + { + value: 'Carrot', + }, ], }, {