Files
edx-platform/lms/djangoapps/instructor/tasks.py
Dillon Dumesnil 05afe78552 fix: AA-1063: Reset Completion data for problems on exam reset
We encountered a bug where learners were sometimes not having their
completion information reset when their exam is reset. It was unclear
what was actually causing the completion to not be reset (it usually
is via a signal listener), but the effect was learners being unable to
reset their due dates in order to attempt the exam again since the exam
believed it was still complete.

This PR will likely be duplicating calls to the Completion API, but we
believe that is worthwhile to ensure successful completion state reset.
2021-10-27 09:42:52 -04:00

103 lines
4.7 KiB
Python

""" Celery Tasks for the Instructor App. """
import logging
from celery import shared_task
from celery_utils.logged_task import LoggedTask
from django.core.exceptions import ObjectDoesNotExist
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from xblock.completable import XBlockCompletionMode
from edx_django_utils.monitoring import set_code_owner_attribute
from common.djangoapps.student.models import get_user_by_username_or_email
from lms.djangoapps.courseware.model_data import FieldDataCache
from lms.djangoapps.courseware.module_render import get_module_for_descriptor
from openedx.core.lib.request_utils import get_request_or_stub
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__)
@shared_task(base=LoggedTask, ignore_result=True)
@set_code_owner_attribute
def update_exam_completion_task(user_identifier: str, content_id: str, completion: float) -> None:
"""
Marks all completable children of content_id as complete for the user.
Submits all completable xblocks inside of the content_id block to the
Completion Service to mark them as complete. One use case of this function is
for special exams (timed/proctored) where regardless of submission status on
individual problems, we want to mark the entire exam as complete when the exam
is finished.
params:
user_identifier (str): username or email of a user
content_id (str): the block key for a piece of content
completion (float): the completion percentage to send to the Completion service (either 1.0 or 0.0)
"""
err_msg_prefix = (
'Error occurred while attempting to complete student attempt for user '
f'{user_identifier} for content_id {content_id}. '
)
err_msg = None
try:
user = get_user_by_username_or_email(user_identifier)
block_key = UsageKey.from_string(content_id)
root_descriptor = modulestore().get_item(block_key)
except ObjectDoesNotExist:
err_msg = err_msg_prefix + 'User does not exist!'
except InvalidKeyError:
err_msg = err_msg_prefix + 'Invalid content_id!'
except ItemNotFoundError:
err_msg = err_msg_prefix + 'Block not found in the modulestore!'
if err_msg:
log.error(err_msg)
return
# This logic has been copied over from openedx/core/djangoapps/schedules/content_highlights.py
# in the _get_course_module function.
# I'm not sure if this is an anti-pattern or not, so if you can avoid re-copying this, please do.
# We are using it here because we ran into issues with the User service being undefined when we
# encountered a split_test xblock.
# Fake a request to fool parts of the courseware that want to inspect it.
request = get_request_or_stub()
request.user = user
# Now evil modulestore magic to inflate our descriptor with user state and
# permissions checks.
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
root_descriptor.course_id, user, root_descriptor, read_only=True,
)
root_module = get_module_for_descriptor(
user, request, root_descriptor, field_data_cache, root_descriptor.course_id,
)
if not root_module:
err_msg = err_msg_prefix + 'Module unable to be created from descriptor!'
log.error(err_msg)
return
def _submit_completions(block, user, completion):
"""
Recursively submits the children for completion to the Completion Service
"""
mode = XBlockCompletionMode.get_mode(block)
if mode == XBlockCompletionMode.COMPLETABLE:
block.runtime.publish(block, 'completion', {'completion': completion, 'user_id': user.id})
elif mode == XBlockCompletionMode.AGGREGATOR:
# I know this looks weird, but at the time of writing at least, there isn't a good
# single way to get the children assigned for a partcular user. Some blocks define the
# child descriptors method, but others don't and with blocks like Randomized Content
# (Library Content), the get_children method returns all children and not just assigned
# children. So this is our way around situations like that. See also Split Test Module
# for another use case where user state has to be taken into account via get_child_descriptors
block_children = ((hasattr(block, 'get_child_descriptors') and block.get_child_descriptors())
or (hasattr(block, 'get_children') and block.get_children())
or [])
for child in block_children:
_submit_completions(child, user, completion)
_submit_completions(root_module, user, completion)