Files
Zachary Hancock 9f16b0f8f6 feat: handle exam submission and reset (#33323)
handles exam events from event bus that impact the 'instructor' app. These apis deal with learner completion and managing problem attempt state.
2023-10-06 09:10:13 -04:00

103 lines
4.8 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 edx_django_utils.monitoring import set_code_owner_attribute
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from xblock.completable import XBlockCompletionMode
from common.djangoapps.student.models import get_user_by_username_or_email
from lms.djangoapps.courseware.block_render import get_block_for_descriptor
from lms.djangoapps.courseware.model_data import FieldDataCache
from openedx.core.lib.request_utils import get_request_or_stub
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
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_block = 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_block 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 block with user state and
# permissions checks.
field_data_cache = FieldDataCache.cache_for_block_descendents(
root_block.scope_ids.usage_id.context_key, user, root_block, read_only=True,
)
root_block = get_block_for_descriptor(
user, request, root_block, field_data_cache, root_block.scope_ids.usage_id.context_key,
)
if not root_block:
err_msg = err_msg_prefix + 'Block 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 blocks 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 Block
# for another use case where user state has to be taken into account via get_child_blocks
block_children = ((hasattr(block, 'get_child_blocks') and block.get_child_blocks())
or (hasattr(block, 'get_children') and block.get_children())
or [])
for child in block_children:
_submit_completions(child, user, completion)
_submit_completions(root_block, user, completion)