diff --git a/scripts/langindex.json b/scripts/langindex.json index 507d94e0bdd..c825692d1d5 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -930,6 +930,7 @@ "addon.mod_quiz.stateoverdue": "quiz", "addon.mod_quiz.stateoverduedetails": "quiz", "addon.mod_quiz.status": "quiz", + "addon.mod_quiz.submission_confirmation_unanswered": "quiz", "addon.mod_quiz.submitallandfinish": "quiz", "addon.mod_quiz.summaryofattempt": "quiz", "addon.mod_quiz.summaryofattempts": "quiz", diff --git a/src/addons/mod/quiz/lang.json b/src/addons/mod/quiz/lang.json index d62d1d3c0fc..dc328c5e1de 100644 --- a/src/addons/mod/quiz/lang.json +++ b/src/addons/mod/quiz/lang.json @@ -68,6 +68,7 @@ "stateoverdue": "Overdue", "stateoverduedetails": "Must be submitted by {{$a}}", "status": "Status", + "submission_confirmation_unanswered": "Questions without a response: {{$a}}", "submitallandfinish": "Submit all and finish", "summaryofattempt": "Summary of attempt", "summaryofattempts": "Summary of your previous attempts", diff --git a/src/addons/mod/quiz/pages/player/player.ts b/src/addons/mod/quiz/pages/player/player.ts index 0ea6316c0a5..6d4aa1aa196 100644 --- a/src/addons/mod/quiz/pages/player/player.ts +++ b/src/addons/mod/quiz/pages/player/player.ts @@ -406,7 +406,34 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { try { // Show confirm if the user clicked the finish button and the quiz is in progress. if (!timeUp && this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { - await CoreDomUtils.showConfirm(Translate.instant('addon.mod_quiz.confirmclose')); + let message = Translate.instant('addon.mod_quiz.confirmclose'); + + const unansweredCount = this.summaryQuestions + .filter(question => AddonModQuiz.isQuestionUnanswered(question)) + .length; + + if (unansweredCount > 0) { + const warning = Translate.instant( + 'addon.mod_quiz.submission_confirmation_unanswered', + { $a: unansweredCount }, + ); + + message += ` + + + + ${ warning } + + + + `; + } + + await CoreDomUtils.showConfirm( + message, + Translate.instant('addon.mod_quiz.submitallandfinish'), + Translate.instant('core.submit'), + ); } modal = await CoreDomUtils.showModalLoading('core.sending', true); diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts index 487e6f388b1..a80da3b489b 100644 --- a/src/addons/mod/quiz/services/quiz.ts +++ b/src/addons/mod/quiz/services/quiz.ts @@ -41,6 +41,7 @@ import { AddonModQuizAttempt } from './quiz-helper'; import { AddonModQuizOffline, AddonModQuizQuestionsWithAnswers } from './quiz-offline'; import { AddonModQuizAutoSyncData, AddonModQuizSyncProvider } from './quiz-sync'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; +import { QUESTION_INVALID_STATE_CLASSES, QUESTION_TODO_STATE_CLASSES } from '@features/question/constants'; const ROOT_CACHE_KEY = 'mmaModQuiz:'; @@ -1517,6 +1518,21 @@ export class AddonModQuizProvider { return !!element.querySelector('.mod_quiz-blocked_question_warning'); } + /** + * Check if a question is unanswered. + * + * @param question Question. + * @returns Whether it's unanswered. + */ + isQuestionUnanswered(question: CoreQuestionQuestionParsed): boolean { + if (!question.stateclass) { + return false; + } + + return QUESTION_TODO_STATE_CLASSES.some(stateClass => stateClass === question.stateclass) + || QUESTION_INVALID_STATE_CLASSES.some(stateClass => stateClass === question.stateclass); + } + /** * Check if a quiz is enabled to be used in offline. * diff --git a/src/addons/mod/quiz/tests/behat/basic-usage-403.feature b/src/addons/mod/quiz/tests/behat/basic-usage-403.feature new file mode 100644 index 00000000000..65d70f103b9 --- /dev/null +++ b/src/addons/mod/quiz/tests/behat/basic-usage-403.feature @@ -0,0 +1,218 @@ +@addon_mod_quiz @app @javascript @lms_from4.0 @lms_upto4.3 +Feature: Attempt a quiz in app + As a student + In order to demonstrate what I know + I need to be able to attempt quizzes + + # These scenarios are duplicated from main because the unanswered questions warning + # is not available before 4.4. + Background: + Given the Moodle site is compatible with this feature + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "users" exist: + | username | + | student1 | + | teacher1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | TF1 | Text of the first question | + | Test questions | truefalse | TF2 | Text of the second question | + And quiz "Quiz 1" contains the following questions: + | question | page | + | TF1 | 1 | + | TF2 | 2 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | quiz | Quiz 2 | Quiz 2 description | C1 | quiz2 | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions 2 | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | multichoice | TF3 | Text of the first question | + | Test questions | shortanswer | TF4 | Text of the second question | + | Test questions | numerical | TF5 | Text of the third question | + | Test questions | essay | TF6 | Text of the fourth question | + | Test questions | ddwtos | TF7 | The [[1]] brown [[2]] jumped over the [[3]] dog. | + | Test questions | truefalse | TF8 | Text of the sixth question | + | Test questions | match | TF9 | Text of the seventh question | + | Test questions | description | TF10 | Text of the eighth question | + # TODO test calculated question type. + # The calculatedsimple type is implemented using the calculated type. + # The calculatedmulti type is implemented using the multichoice type. + # The randomsamatch type is implemented using the match type. + And the following "questions" exist: + | questioncategory | qtype | name | template | + | Test questions | gapselect | TF11 | missingchoiceno | + | Test questions | ddimageortext | TF12 | xsection | + | Test questions | ddmarker | TF13 | mkmap | + And quiz "Quiz 2" contains the following questions: + | question | page | + | TF3 | 1 | + | TF4 | 2 | + | TF5 | 3 | + | TF6 | 4 | + | TF7 | 5 | + | TF8 | 6 | + | TF9 | 7 | + | TF10 | 8 | + | TF11 | 9 | + | TF12 | 10 | + | TF13 | 11 | + + # TODO rewrite using generators. + And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1 + And I add a "Embedded answers (Cloze)" question filling the form with: + | Question name | multianswer | + | Question text | {1:SHORTANSWER:=Berlin} is the capital of Germany. | + | General feedback | The capital of Germany is Berlin. | + And I am on the "quiz2" "Activity" page + And I click on "Questions" "link" + And I click on "Add" "link" + And I click on "from question bank" "link" + And I set the field with xpath "//tr[contains(normalize-space(.), 'multianswer')]//input[@type='checkbox']" to "1" + And I click on "Add selected questions to the quiz" "button" + And I log out + + Scenario: View a quiz entry page (attempts, status, etc.) + Given I entered the quiz activity "Quiz 1" on course "Course 1" as "student1" in the app + When I press "Attempt quiz now" in the app + Then I should find "Text of the first question" in the app + But I should not find "Text of the second question" in the app + + When I press "Next" in the app + Then I should find "Text of the second question" in the app + But I should not find "Text of the first question" in the app + + When I press "Previous" in the app + Then I should find "Text of the first question" in the app + But I should not find "Text of the second question" in the app + + When I press "Next" in the app + Then I should find "Text of the second question" in the app + But I should not find "Text of the first question" in the app + + When I press "Previous" in the app + Then I should find "Text of the first question" in the app + But I should not find "Text of the second question" in the app + + When I press "Next" in the app + And I press "Submit" in the app + Then I should find "Summary of attempt" in the app + + When I press "Not yet answered" within "2" "ion-item" in the app + Then I should find "Text of the second question" in the app + But I should not find "Text of the first question" in the app + + When I press "Submit" in the app + And I press "Submit all and finish" in the app + Then I should find "Once you submit" in the app + + When I press "Cancel" near "Once you submit" in the app + Then I should find "Summary of attempt" in the app + + When I press "Submit all and finish" in the app + And I press "Submit" near "Once you submit" in the app + Then I should find "Review" in the app + And I should find "Started on" in the app + And I should find "State" in the app + And I should find "Completed on" in the app + And I should find "Time taken" in the app + And I should find "Marks" in the app + And I should find "Grade" in the app + And I should find "Question 1" in the app + And I should find "Question 2" in the app + + Scenario: Attempt a quiz (all question types) + Given I entered the quiz activity "Quiz 2" on course "Course 1" as "student1" in the app + When I press "Attempt quiz now" in the app + And I press "Four" in the app + And I press "Three" in the app + And I set the field "Answer" to "Berlin" in the app + And I press "Next" in the app + And I set the field "Answer" to "testing" in the app + And I press "Next" in the app + And I set the field "Answer" to "5" in the app + And I press "Next" in the app + And I set the field "Answer" to "Testing an essay" in the app + And I press "Next" "ion-button" in the app + And I press "quick" ".drag" in the app + And I click on ".place1.drop" "css" + And I press "fox" ".drag" in the app + And I click on ".place2.drop" "css" + And I press "lazy" ".drag" in the app + And I click on ".place3.drop" "css" + And I press "Next" in the app + And I press "True" in the app + And I press "Next" in the app + And I set the field "frog" to "amphibian" in the app + And I set the field "newt" to "insect" in the app + And I set the field "cat" to "mammal" in the app + And I press "Next" in the app + Then I should find "Text of the eighth question" in the app + + When I press "Next" in the app + And I set the field "Blank 1" to "cat" in the app + And I set the field "Blank 2" to "mat" in the app + And I press "Next" in the app + And I press "abyssal" ".drag" in the app + And I click on ".place6.dropzone" "css" + And I press "trench" ".drag" in the app + And I click on ".place3.dropzone" "css" + And I press "Next" in the app + And I press "Railway station" ".marker" in the app + And I click on "img.dropbackground" "css" + And I press "Submit" in the app + Then I should find "Answer saved" in the app + And I should find "Incomplete answer" within "10" "ion-item" in the app + But I should not find "Not yet answered" in the app + + When I press "Submit all and finish" in the app + And I press "Submit" in the app + Then I should find "Review" in the app + And I should find "Finished" in the app + And I should find "Not yet graded" in the app + + When I press "Correct" within "Question 2" "ion-card" in the app + Then I should find "The correct answer is: Berlin" in the app + And I should find "Mark 1.00 out of 1.00" in the app + + Scenario: Submit a quiz & Review a quiz attempt + Given I entered the quiz activity "Quiz 1" on course "Course 1" as "student1" in the app + When I press "Attempt quiz now" in the app + Then I should find "Text of the first question" in the app + And the UI should match the snapshot + + When I press "True" in the app + And I press "Next" in the app + And I press "False" in the app + And I press "Submit" in the app + And I press "Submit all and finish" in the app + And I press "Submit" in the app + Then I should find "Review" in the app + + When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(1) p:nth-child(2)" with "[Started on date]" + And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]" + Then the UI should match the snapshot + + Given I entered the quiz activity "Quiz 1" on course "Course 1" as "teacher1" in the app + When I press "Information" in the app + And I press "Open in browser" in the app + And I switch to the browser tab opened by the app + And I log in as "teacher1" + And I follow "Attempts: 1" + And I follow "Review attempt" + Then I should see "Finished" + And I should see "1.00/2.00" diff --git a/src/addons/mod/quiz/tests/behat/basic_usage-311.feature b/src/addons/mod/quiz/tests/behat/basic_usage-311.feature index 6b4ea8cd78f..2c06b3ce18e 100644 --- a/src/addons/mod/quiz/tests/behat/basic_usage-311.feature +++ b/src/addons/mod/quiz/tests/behat/basic_usage-311.feature @@ -125,7 +125,7 @@ Feature: Attempt a quiz in app Then I should find "Summary of attempt" in the app When I press "Submit all and finish" in the app - And I press "OK" near "Once you submit" in the app + And I press "Submit" near "Once you submit" in the app Then I should find "Review" in the app And I should find "Started on" in the app And I should find "State" in the app @@ -181,7 +181,7 @@ Feature: Attempt a quiz in app But I should not find "Not yet answered" in the app When I press "Submit all and finish" in the app - And I press "OK" in the app + And I press "Submit" in the app Then I should find "Review" in the app And I should find "Finished" in the app And I should find "Not yet graded" in the app @@ -200,7 +200,7 @@ Feature: Attempt a quiz in app And I press "False" in the app And I press "Submit" in the app And I press "Submit all and finish" in the app - And I press "OK" in the app + And I press "Submit" in the app Then I should find "Review" in the app When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(1) p:nth-child(2)" with "[Started on date]" diff --git a/src/addons/mod/quiz/tests/behat/basic_usage-39.feature b/src/addons/mod/quiz/tests/behat/basic_usage-39.feature index 8559851ed66..4004a74cd44 100644 --- a/src/addons/mod/quiz/tests/behat/basic_usage-39.feature +++ b/src/addons/mod/quiz/tests/behat/basic_usage-39.feature @@ -113,7 +113,7 @@ Feature: Attempt a quiz in app And I should find "Incomplete answer" within "9" "ion-item" in the app When I press "Submit all and finish" in the app - And I press "OK" in the app + And I press "Submit" in the app Then I should find "Review" in the app And I should find "Finished" in the app And I should find "Not yet graded" in the app diff --git a/src/addons/mod/quiz/tests/behat/basic_usage.feature b/src/addons/mod/quiz/tests/behat/basic_usage.feature index d0d43ec9f24..f946b71a150 100755 --- a/src/addons/mod/quiz/tests/behat/basic_usage.feature +++ b/src/addons/mod/quiz/tests/behat/basic_usage.feature @@ -117,12 +117,13 @@ Feature: Attempt a quiz in app When I press "Submit" in the app And I press "Submit all and finish" in the app Then I should find "Once you submit" in the app + And I should find "Questions without a response: 2" in the app When I press "Cancel" near "Once you submit" in the app Then I should find "Summary of attempt" in the app When I press "Submit all and finish" in the app - And I press "OK" near "Once you submit" in the app + And I press "Submit" near "Once you submit" in the app Then I should find "Review" in the app And I should find "Started on" in the app And I should find "State" in the app @@ -178,7 +179,9 @@ Feature: Attempt a quiz in app But I should not find "Not yet answered" in the app When I press "Submit all and finish" in the app - And I press "OK" in the app + Then I should find "Questions without a response: 1" in the app + + When I press "Submit" in the app Then I should find "Review" in the app And I should find "Finished" in the app And I should find "Not yet graded" in the app @@ -198,7 +201,10 @@ Feature: Attempt a quiz in app And I press "False" in the app And I press "Submit" in the app And I press "Submit all and finish" in the app - And I press "OK" in the app + Then I should find "Once you submit" in the app + But I should not find "Questions without a response" in the app + + When I press "Submit" in the app Then I should find "Review" in the app When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(1) p:nth-child(2)" with "[Started on date]" diff --git a/src/addons/mod/quiz/tests/behat/quiz_behaviour.feature b/src/addons/mod/quiz/tests/behat/quiz_behaviour.feature index acdc19ccc6e..82282206c11 100644 --- a/src/addons/mod/quiz/tests/behat/quiz_behaviour.feature +++ b/src/addons/mod/quiz/tests/behat/quiz_behaviour.feature @@ -46,7 +46,7 @@ Feature: Use quizzes with different behaviours in the app When I press "Submit" in the app And I press "Submit all and finish" in the app - And I press "OK" near "Once you submit" in the app + And I press "Submit" near "Once you submit" in the app Then I should find "Mark 0.33 out of 1.00" in the app Scenario: Immediate feedback behaviour @@ -85,7 +85,7 @@ Feature: Use quizzes with different behaviours in the app When I press "Submit" in the app And I press "Submit all and finish" in the app - And I press "OK" near "Once you submit" in the app + And I press "Submit" near "Once you submit" in the app Then I should find "Mark 1.00 out of 1.00" in the app Scenario: Deferred feedback with CBM behaviour @@ -103,7 +103,7 @@ Feature: Use quizzes with different behaviours in the app And I press "Quite sure" in the app And I press "Submit" in the app And I press "Submit all and finish" in the app - And I press "OK" near "Once you submit" in the app + And I press "Submit" near "Once you submit" in the app Then I should find "CBM mark 1.50" in the app And I should find "Parts, but only parts, of your response are correct" in the app @@ -147,5 +147,5 @@ Feature: Use quizzes with different behaviours in the app When I press "Submit" in the app And I press "Submit all and finish" in the app - And I press "OK" near "Once you submit" in the app + And I press "Submit" near "Once you submit" in the app Then I should find "Mark 0.33 out of 1.00" in the app diff --git a/src/addons/mod/quiz/tests/behat/quiz_navigation.feature b/src/addons/mod/quiz/tests/behat/quiz_navigation.feature index c1e8f24a2a8..8107f717646 100644 --- a/src/addons/mod/quiz/tests/behat/quiz_navigation.feature +++ b/src/addons/mod/quiz/tests/behat/quiz_navigation.feature @@ -78,7 +78,7 @@ Feature: Navigate through a quiz in the app Then I should find "Summary of attempt" in the app When I press "Submit all and finish" in the app - And I press "OK" near "Once you submit" in the app + And I press "Submit" near "Once you submit" in the app Then I should find "Review" in the app And I should find "Text of the first question" in the app And I should find "Text of the second question" in the app @@ -129,7 +129,7 @@ Feature: Navigate through a quiz in the app # # And I should find "Not yet answered" within "3" "ion-item" in the app # When I press "Submit all and finish" in the app -# And I press "OK" near "Once you submit" in the app +# And I press "Submit" near "Once you submit" in the app # Then I should find "Review" in the app # # @todo MOBILE-4350: Uncomment these. # # And I should find "Text of the first question" in the app diff --git a/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_36.png b/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_38.png similarity index 100% rename from src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_36.png rename to src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_38.png diff --git a/src/core/features/question/constants.ts b/src/core/features/question/constants.ts new file mode 100644 index 00000000000..21b7e6768a5 --- /dev/null +++ b/src/core/features/question/constants.ts @@ -0,0 +1,21 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const QUESTION_TODO_STATE_CLASSES = ['notyetanswered'] as const; +export const QUESTION_INVALID_STATE_CLASSES = ['invalidanswer'] as const; +export const QUESTION_COMPLETE_STATE_CLASSES = ['answersaved'] as const; +export const QUESTION_NEEDS_GRADING_STATE_CLASSES = ['requiresgrading', 'complete'] as const; +export const QUESTION_FINISHED_STATE_CLASSES = ['complete'] as const; +export const QUESTION_GAVE_UP_STATE_CLASSES = ['notanswered'] as const; +export const QUESTION_GRADED_STATE_CLASSES = ['complete', 'incorrect', 'partiallycorrect', 'correct'] as const; diff --git a/src/core/features/question/services/question.ts b/src/core/features/question/services/question.ts index 5f1724d1fcb..0eb50b58985 100644 --- a/src/core/features/question/services/question.ts +++ b/src/core/features/question/services/question.ts @@ -28,6 +28,15 @@ import { QUESTION_ANSWERS_TABLE_NAME, QUESTION_TABLE_NAME, } from './database/question'; +import { + QUESTION_COMPLETE_STATE_CLASSES, + QUESTION_FINISHED_STATE_CLASSES, + QUESTION_GAVE_UP_STATE_CLASSES, + QUESTION_GRADED_STATE_CLASSES, + QUESTION_INVALID_STATE_CLASSES, + QUESTION_NEEDS_GRADING_STATE_CLASSES, + QUESTION_TODO_STATE_CLASSES, +} from '@features/question/constants'; const QUESTION_PREFIX_REGEX = /q\d+:(\d+)_/; const STATES: Record = { @@ -598,6 +607,14 @@ export type CoreQuestionQuestionWSData = { questionnumber?: string; // @since 4.2. Question ordering number in the quiz. state?: string; // The state where the question is in. It won't be returned if the user cannot see it. status?: string; // Current formatted state of the question. + stateclass?: // @since 4.4. A machine-readable class name for the state that this question attempt is in. + typeof QUESTION_TODO_STATE_CLASSES[number] | + typeof QUESTION_INVALID_STATE_CLASSES[number] | + typeof QUESTION_COMPLETE_STATE_CLASSES[number] | + typeof QUESTION_NEEDS_GRADING_STATE_CLASSES[number] | + typeof QUESTION_FINISHED_STATE_CLASSES[number] | + typeof QUESTION_GAVE_UP_STATE_CLASSES[number] | + typeof QUESTION_GRADED_STATE_CLASSES[number]; blockedbyprevious?: boolean; // Whether the question is blocked by the previous question. mark?: string; // The mark awarded. It will be returned only if the user is allowed to see it. maxmark?: number; // The maximum mark possible for this question attempt. diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 9f229e7b752..c6eed8e06e4 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -514,6 +514,12 @@ ion-alert { .alert-message { user-select: text; flex-shrink: 0; + + ion-card { + margin: 0; + margin-top: 10px; + } + } }