AA-261: Overridding Show Answer functionality for PLS courses

We noticed that since users can choose to reset their due dates,
they would have the ability to let due dates pass and then for any
assessment that allows viewing the answer after the due date would be
visible. The user could thus view all answers and then reset their
due dates to receive a perfect score.

This PR works to fix that issue by changing all show answer values
to not take into account being past the due date when inside a PLS
course.
This commit is contained in:
Dillon Dumesnil
2020-07-28 13:56:08 -07:00
parent 3d1148b874
commit 75f6937003
8 changed files with 149 additions and 5 deletions

View File

@@ -0,0 +1,56 @@
"""
FieldOverride that forces Show Answer values that use Past Due logic to
new Show Answer values that remove the Past Due check (keeping the rest intact)
"""
from django.conf import settings
from common.lib.xmodule.xmodule.capa_base import SHOWANSWER
from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider
from openedx.features.course_experience import RELATIVE_DATES_FLAG
class ShowAnswerFieldOverride(FieldOverrideProvider):
"""
A concrete implementation of
:class:`~courseware.field_overrides.FieldOverrideProvider` which forces
Show Answer values that use Past Due logic to new Show Answer values
that remove the Past Due check (keeping the rest intact)
Once Courseware is able to use BlockTransformers, this override should be
converted to a BlockTransformer to set the showanswer field.
"""
def get(self, block, name, default):
"""
Overwrites the 'showanswer' field on blocks in self-paced courses to
remove any checks about due dates being in the past.
"""
if name != 'showanswer':
return default
has_showanswer = self.fallback_field_data.has(block, 'showanswer')
# This is to explicitly check the case where the default value of
# SHOWANSWER.FINISHED is left on a Course. In that case, we continue
# to follow the same mapping of FINISHED -> AFTER_ALL_ATTEMPTS_OR_CORRECT.
# This value will then be inherited throughout the rest of the Course.
if not has_showanswer and block.category == 'course':
return SHOWANSWER.AFTER_ALL_ATTEMPTS_OR_CORRECT
elif not has_showanswer:
return default
mapping = {
SHOWANSWER.ATTEMPTED: SHOWANSWER.ATTEMPTED_NO_PAST_DUE,
SHOWANSWER.CLOSED: SHOWANSWER.AFTER_ALL_ATTEMPTS,
SHOWANSWER.CORRECT_OR_PAST_DUE: SHOWANSWER.ANSWERED,
SHOWANSWER.FINISHED: SHOWANSWER.AFTER_ALL_ATTEMPTS_OR_CORRECT,
SHOWANSWER.PAST_DUE: SHOWANSWER.NEVER,
}
current_show_answer_value = self.fallback_field_data.get(block, 'showanswer')
return mapping.get(current_show_answer_value, default)
@classmethod
def enabled_for(cls, course):
""" Enabled only for Self-Paced courses using Personalized User Schedules. """
return course.self_paced and RELATIVE_DATES_FLAG.is_enabled(course.id)

View File

@@ -0,0 +1,67 @@
"""Tests for Show Answer overrides for self-paced courses."""
import ddt
from django.test import RequestFactory
from django.test.utils import override_settings
from common.lib.xmodule.xmodule.capa_base import SHOWANSWER
from lms.djangoapps.ccx.tests.test_overrides import inject_field_overrides
from lms.djangoapps.courseware.model_data import FieldDataCache
from lms.djangoapps.courseware.module_render import get_module
from openedx.features.course_experience import RELATIVE_DATES_FLAG
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@override_settings(
FIELD_OVERRIDE_PROVIDERS=[
'openedx.features.personalized_learner_schedules.show_answer.show_answer_field_override.ShowAnswerFieldOverride'
],
)
@ddt.ddt
class ShowAnswerFieldOverrideTest(ModuleStoreTestCase):
""" Tests for Show Answer overrides for self-paced courses. """
def setup_course(self, **course_kwargs):
""" Set up a course with provided course attributes. """
course = CourseFactory.create(**course_kwargs)
inject_field_overrides((course,), course, self.user)
return course
def get_course_module(self, course):
request = RequestFactory().request()
field_data_cache = FieldDataCache([], course.id, self.user)
return get_module(self.user, request, course.location, field_data_cache, course=course)
@ddt.data(True, False)
def test_override_enabled_for(self, active):
with RELATIVE_DATES_FLAG.override(active=active):
# Instructor paced course will just have the default value
ip_course = self.setup_course()
course_module = self.get_course_module(ip_course)
self.assertEqual(course_module.showanswer, SHOWANSWER.FINISHED)
sp_course = self.setup_course(self_paced=True)
course_module = self.get_course_module(sp_course)
if active:
self.assertEqual(course_module.showanswer, SHOWANSWER.AFTER_ALL_ATTEMPTS_OR_CORRECT)
else:
self.assertEqual(course_module.showanswer, SHOWANSWER.FINISHED)
@ddt.data(
(SHOWANSWER.ATTEMPTED, SHOWANSWER.ATTEMPTED_NO_PAST_DUE),
(SHOWANSWER.CLOSED, SHOWANSWER.AFTER_ALL_ATTEMPTS),
(SHOWANSWER.CORRECT_OR_PAST_DUE, SHOWANSWER.ANSWERED),
(SHOWANSWER.FINISHED, SHOWANSWER.AFTER_ALL_ATTEMPTS_OR_CORRECT),
(SHOWANSWER.PAST_DUE, SHOWANSWER.NEVER),
(SHOWANSWER.NEVER, SHOWANSWER.NEVER),
(SHOWANSWER.AFTER_SOME_NUMBER_OF_ATTEMPTS, SHOWANSWER.AFTER_SOME_NUMBER_OF_ATTEMPTS),
(SHOWANSWER.ALWAYS, SHOWANSWER.ALWAYS),
)
@ddt.unpack
@RELATIVE_DATES_FLAG.override(active=True)
def test_get(self, initial_value, expected_final_value):
course = self.setup_course(self_paced=True, showanswer=initial_value)
course_module = self.get_course_module(course)
self.assertEqual(course_module.showanswer, expected_final_value)