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

Create a runtime service for answer_available function #33424

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 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
10 changes: 8 additions & 2 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.partitions.partitions_service import get_all_partitions_for_course # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.services import SettingsService, ConfigurationService, TeamsConfigurationService
from xmodule.services import (
SettingsService,
ConfigurationService,
TeamsConfigurationService,
ResultService
steff456 marked this conversation as resolved.
Show resolved Hide resolved
)


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -1183,7 +1188,8 @@ def load_services_for_studio(runtime, user):
"settings": SettingsService(),
"lti-configuration": ConfigurationService(CourseAllowPIISharingInLTIFlag),
"teams_configuration": TeamsConfigurationService(),
"library_tools": LibraryToolsService(modulestore(), user.id)
"library_tools": LibraryToolsService(modulestore(), user.id),
"result": ResultService,
steff456 marked this conversation as resolved.
Show resolved Hide resolved
}

runtime._services.update(services) # lint-amnesty, pylint: disable=protected-access
Expand Down
5 changes: 3 additions & 2 deletions cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore.django import XBlockI18nService, modulestore
from xmodule.partitions.partitions_service import PartitionService
from xmodule.services import SettingsService, TeamsConfigurationService
from xmodule.services import SettingsService, TeamsConfigurationService, ResultService
from xmodule.studio_editable import has_author_view
from xmodule.util.sandboxing import SandboxService
from xmodule.util.builtin_assets import add_webpack_js_to_fragment
Expand Down Expand Up @@ -212,7 +212,8 @@ def _prepare_runtime_for_preview(request, block):
"teams_configuration": TeamsConfigurationService(),
"sandbox": SandboxService(contentstore=contentstore, course_id=course_id),
"cache": CacheService(cache),
'replace_urls': ReplaceURLService
'replace_urls': ReplaceURLService,
"result": partial(ResultService, user_is_staff=True),
}

block.runtime.get_block_for_descriptor = partial(_load_preview_block, request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@
from openedx.core.lib.gating import api as gating_api
from openedx.core.lib.cache_utils import request_cached
from openedx.core.toggles import ENTRANCE_EXAMS
from xmodule.services import (
ConfigurationService,
SettingsService,
TeamsConfigurationService,
ResultService
)
from xmodule.course_block import DEFAULT_START_DATE
from xmodule.library_tools import LibraryToolsService
from xmodule.modulestore import EdxJSONEncoder, ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata
from xmodule.services import ConfigurationService, SettingsService, TeamsConfigurationService
from xmodule.tabs import CourseTabList

from ..utils import (
Expand Down Expand Up @@ -330,6 +335,7 @@ def load_services_for_studio(runtime, user):
"lti-configuration": ConfigurationService(CourseAllowPIISharingInLTIFlag),
"teams_configuration": TeamsConfigurationService(),
"library_tools": LibraryToolsService(modulestore(), user.id),
"result": ResultService,
}

runtime._services.update(services) # lint-amnesty, pylint: disable=protected-access
Expand Down
9 changes: 8 additions & 1 deletion lms/djangoapps/courseware/block_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.partitions.partitions_service import PartitionService
from xmodule.util.sandboxing import SandboxService
from xmodule.services import EventPublishingService, RebindUserService, SettingsService, TeamsConfigurationService
from xmodule.services import (
EventPublishingService,
RebindUserService,
SettingsService,
TeamsConfigurationService,
ResultService
)
from common.djangoapps.static_replace.services import ReplaceURLService
from common.djangoapps.static_replace.wrapper import replace_urls_wrapper
from lms.djangoapps.courseware.access import get_user_role, has_access
Expand Down Expand Up @@ -632,6 +638,7 @@ def inner_get_block(block: XBlock) -> XBlock | None:
'teams_configuration': TeamsConfigurationService(),
'call_to_action': CallToActionService(),
'publish': EventPublishingService(user, course_id, track_function),
'result': partial(ResultService, user_is_staff=user_is_staff)
}

runtime.get_block_for_descriptor = inner_get_block
Expand Down
2 changes: 2 additions & 0 deletions lms/djangoapps/courseware/tests/test_block_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
@XBlock.needs('teams')
@XBlock.needs('teams_configuration')
@XBlock.needs('call_to_action')
@XBlock.needs('result')
class PureXBlock(XBlock):
"""
Pure XBlock to use in tests.
Expand Down Expand Up @@ -2340,6 +2341,7 @@ class LMSXBlockServiceBindingTest(LMSXBlockServiceMixin):
'teams',
'teams_configuration',
'call_to_action',
'result',
)
def test_expected_services_exist(self, expected_service):
"""
Expand Down
6 changes: 4 additions & 2 deletions lms/djangoapps/grades/subsection_grade.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
from lms.djangoapps.grades.scores import compute_percent, get_score, possibly_scored
from xmodule import block_metadata_utils, graders # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.graders import AggregatedScore, ShowCorrectness # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.graders import AggregatedScore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.services import ResultService

log = getLogger(__name__)

Expand All @@ -23,6 +24,7 @@ class SubsectionGradeBase(metaclass=ABCMeta):
"""

def __init__(self, subsection):
self._block = subsection
self.location = subsection.location
self.display_name = block_metadata_utils.display_name_with_default(subsection)
self.url_name = block_metadata_utils.url_name_for_block(subsection)
Expand Down Expand Up @@ -59,7 +61,7 @@ def show_grades(self, has_staff_access):
"""
Returns whether subsection scores are currently available to users with or without staff access.
"""
return ShowCorrectness.correctness_available(self.show_correctness, self.due, has_staff_access)
return ResultService(block=self._block, user_is_staff=has_staff_access).correctness_available()

@property
def attempted_graded(self):
Expand Down
4 changes: 3 additions & 1 deletion openedx/core/djangoapps/xblock/runtime/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from xmodule.errortracker import make_error_tracker
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import XBlockI18nService
from xmodule.services import EventPublishingService, RebindUserService
from xmodule.services import EventPublishingService, RebindUserService, ResultService
from xmodule.util.sandboxing import SandboxService
from common.djangoapps.edxmako.services import MakoService
from common.djangoapps.static_replace.services import ReplaceURLService
Expand Down Expand Up @@ -299,6 +299,8 @@ def service(self, block: XBlock, service_name: str):
)
elif service_name == 'publish':
return EventPublishingService(self.user, context_key, make_track_function())
elif service_name == 'result':
return ResultService(block=block, user_is_staff=self.user.is_staff)

# Check if the XBlockRuntimeSystem wants to handle this:
service = self.system.get_service(block, service_name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider
from openedx.features.course_experience import RELATIVE_DATES_FLAG
from xmodule.capa_block import SHOWANSWER # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.graders import ShowAnswer


class ShowAnswerFieldOverride(FieldOverrideProvider):
Expand All @@ -27,11 +27,11 @@ def get(self, block, name, default):
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,
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')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from lms.djangoapps.courseware.model_data import FieldDataCache
from lms.djangoapps.courseware.block_render import get_block
from openedx.features.course_experience import RELATIVE_DATES_FLAG
from xmodule.capa_block import SHOWANSWER # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.graders import ShowAnswer
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order

Expand Down Expand Up @@ -41,27 +41,27 @@ def test_override_enabled_for(self, active):
# Instructor paced course will just have the default value
ip_course = self.setup_course()
course_block = self.get_course_block(ip_course)
assert course_block.showanswer == SHOWANSWER.FINISHED
assert course_block.showanswer == ShowAnswer.FINISHED

# This should be updated to not explicitly add in the showanswer so it can test the
# default case of never touching showanswer. Reference ticket AA-307 (if that's closed,
# this can be updated!)
sp_course = self.setup_course(self_paced=True, showanswer=SHOWANSWER.FINISHED)
sp_course = self.setup_course(self_paced=True, showanswer=ShowAnswer.FINISHED)
course_block = self.get_course_block(sp_course)
if active:
assert course_block.showanswer == SHOWANSWER.AFTER_ALL_ATTEMPTS_OR_CORRECT
assert course_block.showanswer == ShowAnswer.AFTER_ALL_ATTEMPTS_OR_CORRECT
else:
assert course_block.showanswer == SHOWANSWER.FINISHED
assert course_block.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),
(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
@override_waffle_flag(RELATIVE_DATES_FLAG, active=True)
Expand Down
96 changes: 17 additions & 79 deletions xmodule/capa_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from xmodule.contentstore.django import contentstore
from xmodule.editing_block import EditingMixin
from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.graders import ShowCorrectness
from xmodule.graders import ShowCorrectness, ShowAnswer
from xmodule.raw_block import RawMixin
from xmodule.util.sandboxing import SandboxService
from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_sass_to_fragment
Expand Down Expand Up @@ -74,24 +74,6 @@
FEATURES = {}


class SHOWANSWER:
steff456 marked this conversation as resolved.
Show resolved Hide resolved
"""
Constants for when to show answer
"""
ALWAYS = "always"
ANSWERED = "answered"
ATTEMPTED = "attempted"
CLOSED = "closed"
FINISHED = "finished"
CORRECT_OR_PAST_DUE = "correct_or_past_due"
PAST_DUE = "past_due"
NEVER = "never"
AFTER_SOME_NUMBER_OF_ATTEMPTS = "after_attempts"
AFTER_ALL_ATTEMPTS = "after_all_attempts"
AFTER_ALL_ATTEMPTS_OR_CORRECT = "after_all_attempts_or_correct"
ATTEMPTED_NO_PAST_DUE = "attempted_no_past_due"


steff456 marked this conversation as resolved.
Show resolved Hide resolved
class RANDOMIZATION:
"""
Constants for problem randomization
Expand Down Expand Up @@ -123,6 +105,7 @@ def from_json(self, value):
@XBlock.needs('sandbox')
@XBlock.needs('replace_urls')
@XBlock.wants('call_to_action')
@XBlock.wants('result')
steff456 marked this conversation as resolved.
Show resolved Hide resolved
class ProblemBlock(
ScorableXBlockMixin,
RawMixin,
Expand Down Expand Up @@ -203,20 +186,20 @@ class ProblemBlock(
help=_("Defines when to show the answer to the problem. "
"A default value can be set in Advanced Settings."),
scope=Scope.settings,
default=SHOWANSWER.FINISHED,
default=ShowAnswer.FINISHED,
values=[
{"display_name": _("Always"), "value": SHOWANSWER.ALWAYS},
{"display_name": _("Answered"), "value": SHOWANSWER.ANSWERED},
{"display_name": _("Attempted or Past Due"), "value": SHOWANSWER.ATTEMPTED},
{"display_name": _("Closed"), "value": SHOWANSWER.CLOSED},
{"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": _("After Some Number of Attempts"), "value": SHOWANSWER.AFTER_SOME_NUMBER_OF_ATTEMPTS},
{"display_name": _("After All Attempts"), "value": SHOWANSWER.AFTER_ALL_ATTEMPTS},
{"display_name": _("After All Attempts or Correct"), "value": SHOWANSWER.AFTER_ALL_ATTEMPTS_OR_CORRECT},
{"display_name": _("Attempted"), "value": SHOWANSWER.ATTEMPTED_NO_PAST_DUE},
{"display_name": _("Always"), "value": ShowAnswer.ALWAYS},
{"display_name": _("Answered"), "value": ShowAnswer.ANSWERED},
{"display_name": _("Attempted or Past Due"), "value": ShowAnswer.ATTEMPTED},
{"display_name": _("Closed"), "value": ShowAnswer.CLOSED},
{"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": _("After Some Number of Attempts"), "value": ShowAnswer.AFTER_SOME_NUMBER_OF_ATTEMPTS},
{"display_name": _("After All Attempts"), "value": ShowAnswer.AFTER_ALL_ATTEMPTS},
{"display_name": _("After All Attempts or Correct"), "value": ShowAnswer.AFTER_ALL_ATTEMPTS_OR_CORRECT},
{"display_name": _("Attempted"), "value": ShowAnswer.ATTEMPTED_NO_PAST_DUE},
]
)
attempts_before_showanswer_button = Integer(
Expand Down Expand Up @@ -1406,60 +1389,15 @@ def answer_available(self):
"""
Is the user allowed to see an answer?
"""
user_is_staff = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF)
if not self.correctness_available():
# If correctness is being withheld, then don't show answers either.
return False
elif self.showanswer == '':
return False
elif self.showanswer == SHOWANSWER.NEVER:
return False
elif user_is_staff:
# This is after the 'never' check because admins can see the answer
# unless the problem explicitly prevents it
return True
elif self.showanswer == SHOWANSWER.ATTEMPTED:
return self.is_attempted() or self.is_past_due()
elif self.showanswer == SHOWANSWER.ANSWERED:
# NOTE: this is slightly different from 'attempted' -- resetting the problems
# makes lcp.done False, but leaves attempts unchanged.
return self.is_correct()
elif self.showanswer == SHOWANSWER.CLOSED:
return self.closed()
elif self.showanswer == SHOWANSWER.FINISHED:
return self.closed() or self.is_correct()

elif self.showanswer == SHOWANSWER.CORRECT_OR_PAST_DUE:
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
elif self.showanswer == SHOWANSWER.AFTER_ALL_ATTEMPTS:
return self.used_all_attempts()
elif self.showanswer == SHOWANSWER.AFTER_ALL_ATTEMPTS_OR_CORRECT:
return self.used_all_attempts() or self.is_correct()
elif self.showanswer == SHOWANSWER.ATTEMPTED_NO_PAST_DUE:
return self.is_attempted()
return False
return self.runtime.service(self, 'result').answer_available()

def correctness_available(self):
"""
Is the user allowed to see whether she's answered correctly?

Limits access to the correct/incorrect flags, messages, and problem score.
"""
user_is_staff = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF)
return ShowCorrectness.correctness_available(
show_correctness=self.show_correctness,
due_date=self.close_date,
has_staff_access=user_is_staff,
)
return self.runtime.service(self, 'result').correctness_available()

def update_score(self, data):
"""
Expand Down
Loading
Loading