From 4a98e4626aefd74276e3b7e546c31d5907e13802 Mon Sep 17 00:00:00 2001 From: Joseph Okonda Date: Fri, 30 Jun 2017 12:16:23 -0700 Subject: [PATCH 1/2] Add new Option to Show Answer Dropdown. In Studio: Introduces a new option, `after some number of attempts` and a new entry box for specifying the number of attempts. This allows course creators to specify that a given question's answer is only viewable, i.e its show answer button is visible, after the learner has attempted answering the question - by hitting the submit button - a given number of times. Included in this commit are unit tests for the new feature. --- common/lib/xmodule/xmodule/capa_base.py | 16 ++- .../xmodule/xmodule/capa_base_constants.py | 1 + .../xmodule/xmodule/tests/test_capa_module.py | 109 ++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/capa_base.py b/common/lib/xmodule/xmodule/capa_base.py index d0c13a9636..0aad0f3a80 100644 --- a/common/lib/xmodule/xmodule/capa_base.py +++ b/common/lib/xmodule/xmodule/capa_base.py @@ -139,7 +139,16 @@ class CapaFields(object): {"display_name": _("Finished"), "value": SHOWANSWER.FINISHED}, {"display_name": _("Correct or Past Due"), "value": SHOWANSWER.CORRECT_OR_PAST_DUE}, {"display_name": _("Past Due"), "value": SHOWANSWER.PAST_DUE}, - {"display_name": _("Never"), "value": SHOWANSWER.NEVER}] + {"display_name": _("Never"), "value": SHOWANSWER.NEVER}, + {"display_name": _("After Some Number of Attempts"), "value": SHOWANSWER.AFTER_SOME_NUMBER_OF_ATTEMPTS}, + ] + ) + attempts_before_showanswer_button = Integer( + display_name=_("Show Answer: Number of Attempts"), + help=_("Number of times the student must attempt to answer the question before the Show Answer button appears."), + values={"min": 0}, + default=0, + scope=Scope.settings, ) force_save_button = Boolean( help=_("Whether to force the save button to appear on the page"), @@ -913,6 +922,11 @@ class CapaMixin(ScorableXBlockMixin, CapaFields): return self.is_correct() or self.is_past_due() elif self.showanswer == SHOWANSWER.PAST_DUE: return self.is_past_due() + elif self.showanswer == SHOWANSWER.AFTER_SOME_NUMBER_OF_ATTEMPTS: + required_attempts = self.attempts_before_showanswer_button + if self.max_attempts and required_attempts >= self.max_attempts: + required_attempts = self.max_attempts + return self.attempts >= required_attempts elif self.showanswer == SHOWANSWER.ALWAYS: return True diff --git a/common/lib/xmodule/xmodule/capa_base_constants.py b/common/lib/xmodule/xmodule/capa_base_constants.py index 7739be238e..380ec1885d 100644 --- a/common/lib/xmodule/xmodule/capa_base_constants.py +++ b/common/lib/xmodule/xmodule/capa_base_constants.py @@ -16,6 +16,7 @@ class SHOWANSWER(object): CORRECT_OR_PAST_DUE = "correct_or_past_due" PAST_DUE = "past_due" NEVER = "never" + AFTER_SOME_NUMBER_OF_ATTEMPTS = "after_attempts" class RANDOMIZATION(object): diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 986ce57531..73c2520f90 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -415,6 +415,115 @@ class CapaModuleTest(unittest.TestCase): graceperiod=self.two_day_delta_str) self.assertFalse(still_in_grace.answer_available()) + def test_showanswer_after_attempts_with_max(self): + """ + Button should not be visible when attempts < required attempts. + + Even with max attempts set, the show answer button should only + show up after the user has attempted answering the question for + the requisite number of times, i.e `attempts_before_showanswer_button` + """ + problem = CapaFactory.create( + showanswer='after_attempts', + attempts='2', + attempts_before_showanswer_button='3', + max_attempts='5', + ) + self.assertFalse(problem.answer_available()) + + def test_showanswer_after_attempts_no_max(self): + """ + Button should not be visible when attempts < required attempts. + + Even when max attempts is NOT set, the answer should still + only be available after the student has attempted the + problem at least `attempts_before_showanswer_button` times + """ + problem = CapaFactory.create( + showanswer='after_attempts', + attempts='2', + attempts_before_showanswer_button='3', + ) + self.assertFalse(problem.answer_available()) + + def test_showanswer_after_attempts_used_all_attempts(self): + """ + Button should be visible even after all attempts are used up. + + As long as the student has attempted the question for + the requisite number of times, then the show ans. button is + visible even after they have exhausted their attempts. + """ + problem = CapaFactory.create( + showanswer='after_attempts', + attempts_before_showanswer_button='2', + max_attempts='3', + attempts='3', + due=self.tomorrow_str, + ) + self.assertTrue(problem.answer_available()) + + def test_showanswer_after_attempts_past_due_date(self): + """ + Show Answer button should be visible even after the due date. + + As long as the student has attempted the problem for the requisite + number of times, the answer should be available past the due date. + """ + problem = CapaFactory.create( + showanswer='after_attempts', + attempts_before_showanswer_button='2', + attempts='2', + due=self.yesterday_str, + ) + self.assertTrue(problem.answer_available()) + + def test_showanswer_after_attempts_still_in_grace(self): + """ + If attempts > required attempts, ans. is available in grace period. + + As long as the user has attempted for the requisite # of times, + the show answer button is visible throughout the grace period. + """ + problem = CapaFactory.create( + showanswer='after_attempts', + after_attempts='3', + attempts='4', + due=self.yesterday_str, + graceperiod=self.two_day_delta_str, + ) + self.assertTrue(problem.answer_available()) + + def test_showanswer_after_attempts_large(self): + """ + If required attempts > max attempts then required attempts = max attempts. + + Ensure that if attempts_before_showanswer_button > max_attempts, + the button should show up after all attempts are used up, + i.e after_attempts falls back to max_attempts + """ + problem = CapaFactory.create( + showanswer='after_attempts', + attempts_before_showanswer_button='5', + max_attempts='3', + attempts='3', + ) + self.assertTrue(problem.answer_available()) + + def test_showanswer_after_attempts_zero(self): + """ + Button should always be visible if required min attempts = 0. + + If attempts_before_showanswer_button = 0, then the show answer + button should be visible at all times. + """ + problem = CapaFactory.create( + showanswer='after_attempts', + attempts_before_showanswer_button='0', + attempts='0', + ) + self.assertTrue(problem.answer_available()) + def test_showanswer_finished(self): """ With showanswer="finished" should show answer after the problem is closed, From e7733faa1a048258621386798c1b51e7658f1199 Mon Sep 17 00:00:00 2001 From: stv Date: Wed, 12 Dec 2018 15:54:07 -0800 Subject: [PATCH 2/2] Fix tests: Show Answer, Number of Attempts --- cms/djangoapps/contentstore/features/problem-editor.py | 2 ++ .../test/acceptance/tests/studio/test_studio_problem_editor.py | 1 + 2 files changed, 3 insertions(+) diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index 4df8d871b7..72b46a733b 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -16,6 +16,7 @@ MAXIMUM_ATTEMPTS = "Maximum Attempts" PROBLEM_WEIGHT = "Problem Weight" RANDOMIZATION = 'Randomization' SHOW_ANSWER = "Show Answer" +SHOW_ANSWER_AFTER_SOME_NUMBER_OF_ATTEMPTS = 'Show Answer: Number of Attempts' SHOW_RESET_BUTTON = "Show Reset Button" TIMER_BETWEEN_ATTEMPTS = "Timer Between Attempts" MATLAB_API_KEY = "Matlab API key" @@ -106,6 +107,7 @@ def i_see_advanced_settings_with_values(_step): [PROBLEM_WEIGHT, "", False], [RANDOMIZATION, "Never", False], [SHOW_ANSWER, "Finished", False], + [SHOW_ANSWER_AFTER_SOME_NUMBER_OF_ATTEMPTS, '0', False], [SHOW_RESET_BUTTON, "False", False], [TIMER_BETWEEN_ATTEMPTS, "0", False], ]) diff --git a/common/test/acceptance/tests/studio/test_studio_problem_editor.py b/common/test/acceptance/tests/studio/test_studio_problem_editor.py index fe700b7e72..9d9c630ddf 100644 --- a/common/test/acceptance/tests/studio/test_studio_problem_editor.py +++ b/common/test/acceptance/tests/studio/test_studio_problem_editor.py @@ -56,6 +56,7 @@ class ProblemComponentEditor(ContainerBase): 'Problem Weight': u'', 'Randomization': u'Never', 'Show Answer': u'Finished', + 'Show Answer: Number of Attempts': u'0', 'Show Reset Button': u'False', 'Timer Between Attempts': u'0' }