161 lines
7.2 KiB
Python
161 lines
7.2 KiB
Python
"""
|
||
Creates Call to Actions for resetting a Personalized Learner Schedule for use inside of Courseware.
|
||
"""
|
||
|
||
import logging
|
||
|
||
from crum import get_current_request
|
||
|
||
from django.conf import settings
|
||
from django.urls import reverse
|
||
from django.utils.translation import ngettext, gettext as _
|
||
|
||
from common.lib.xmodule.xmodule.util.misc import is_xblock_an_assignment
|
||
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
|
||
from openedx.features.course_experience.url_helpers import is_request_from_learning_mfe
|
||
from openedx.features.course_experience.utils import dates_banner_should_display
|
||
|
||
log = logging.getLogger(__name__)
|
||
|
||
|
||
class PersonalizedLearnerScheduleCallToAction:
|
||
"""
|
||
Creates Call to Actions for resetting a Personalized Learner Schedule for use inside of Courseware.
|
||
"""
|
||
CAPA_SUBMIT_DISABLED = 'capa_submit_disabled'
|
||
VERTICAL_BANNER = 'vertical_banner'
|
||
past_due_class_warnings = set()
|
||
|
||
def get_ctas(self, xblock, category, completed):
|
||
"""
|
||
Return the calls to action associated with the specified category for the given xblock.
|
||
|
||
Look at CallToActionService docstring to see what will be returned.
|
||
"""
|
||
ctas = []
|
||
request = get_current_request()
|
||
|
||
course_key = xblock.scope_ids.usage_id.context_key
|
||
missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
|
||
# Not showing in the missed_gated_content case because those learners are not eligible
|
||
# to shift due dates.
|
||
if not missed_deadlines or missed_gated_content:
|
||
return []
|
||
|
||
# Some checks to disable PLS calls to action until these environments (mobile and MFE) support them natively
|
||
if request and is_request_from_mobile_app(request):
|
||
return []
|
||
|
||
is_learning_mfe = request and is_request_from_learning_mfe(request)
|
||
if category == self.CAPA_SUBMIT_DISABLED:
|
||
# xblock is a capa problem, and the submit button is disabled. Check if it's because of a personalized
|
||
# schedule due date being missed, and if so, we can offer to shift it.
|
||
if self._is_block_shiftable(xblock, category):
|
||
ctas.append(self._make_reset_deadlines_cta(xblock, category, is_learning_mfe))
|
||
|
||
elif category == self.VERTICAL_BANNER and not completed:
|
||
# xblock is a vertical, so we'll check all the problems inside it. If there are any that will show a
|
||
# a "shift dates" CTA under CAPA_SUBMIT_DISABLED, then we'll also show the same CTA as a vertical banner.
|
||
if any(self._is_block_shiftable(item, category) for item in xblock.get_display_items()):
|
||
ctas.append(self._make_reset_deadlines_cta(xblock, category, is_learning_mfe))
|
||
|
||
return ctas
|
||
|
||
@classmethod
|
||
def _is_block_shiftable(cls, xblock, category):
|
||
"""
|
||
Test if the xblock would be solvable if we were to shift dates.
|
||
|
||
Only xblocks with an is_past_due method (e.g. capa and LTI) will be considered possibly shiftable.
|
||
"""
|
||
if not hasattr(xblock, 'is_past_due'):
|
||
return False
|
||
|
||
if hasattr(xblock, 'attempts') and hasattr(xblock, 'max_attempts'):
|
||
can_attempt = xblock.max_attempts is None or xblock.attempts < xblock.max_attempts
|
||
else:
|
||
can_attempt = True
|
||
|
||
if callable(xblock.is_past_due):
|
||
is_past_due = xblock.is_past_due()
|
||
else:
|
||
PersonalizedLearnerScheduleCallToAction._log_past_due_warning(type(xblock).__name__)
|
||
is_past_due = xblock.is_past_due
|
||
|
||
can_shift = xblock.self_paced and can_attempt and is_past_due
|
||
|
||
# Note: we will still show the CTA at the xblock level (next to the submit button) regardless
|
||
# of if the xblock is an assignment (meaning graded *and* scored)
|
||
if category == cls.VERTICAL_BANNER:
|
||
can_shift = can_shift and is_xblock_an_assignment(xblock)
|
||
|
||
return can_shift
|
||
|
||
@staticmethod
|
||
def _log_past_due_warning(name):
|
||
"""
|
||
Logs out if an xblock has is_past_due defined as a property
|
||
(since we want to move to using it as a function everywhere)
|
||
"""
|
||
if name in PersonalizedLearnerScheduleCallToAction.past_due_class_warnings:
|
||
return
|
||
|
||
log.warning('PersonalizedLearnerScheduleCallToAction has encountered an xblock that defines is_past_due '
|
||
'as a property. This is supported for now, but may not be in the future. Please change '
|
||
'%s.is_past_due into a method.', name)
|
||
PersonalizedLearnerScheduleCallToAction.past_due_class_warnings.add(name)
|
||
|
||
@classmethod
|
||
def _make_reset_deadlines_cta(cls, xblock, category, is_learning_mfe=False):
|
||
"""
|
||
Constructs a call to action object containing the necessary information for the view
|
||
"""
|
||
from lms.urls import RESET_COURSE_DEADLINES_NAME
|
||
course_key = xblock.scope_ids.usage_id.context_key
|
||
|
||
cta_data = {
|
||
'link': reverse(RESET_COURSE_DEADLINES_NAME),
|
||
'link_name': _('Shift due dates'),
|
||
'form_values': {
|
||
'course_id': course_key,
|
||
},
|
||
'description': _('To participate in this assignment, the suggested schedule for your course needs '
|
||
'updating. Don’t worry, we’ll shift all the due dates for you and you won’t lose '
|
||
'any of your progress.'),
|
||
}
|
||
|
||
has_attempts = hasattr(xblock, 'attempts') and hasattr(xblock, 'max_attempts')
|
||
|
||
if category == cls.CAPA_SUBMIT_DISABLED and has_attempts and xblock.attempts:
|
||
if xblock.max_attempts:
|
||
cta_data['link_name'] = ngettext('Try again ({attempts} attempt remaining)',
|
||
'Try again ({attempts} attempts remaining)',
|
||
(xblock.max_attempts - xblock.attempts)).format(
|
||
attempts=(xblock.max_attempts - xblock.attempts)
|
||
)
|
||
cta_data['description'] = (_('You have used {attempts} of {max_attempts} attempts for this '
|
||
'problem.').format(
|
||
attempts=xblock.attempts, max_attempts=xblock.max_attempts
|
||
) + ' ' + cta_data['description'])
|
||
else:
|
||
cta_data['link_name'] = _('Try again (unlimited attempts)')
|
||
cta_data['description'] = _('You have used {attempts} of unlimited attempts for this '
|
||
'problem.').format(attempts=xblock.attempts) + ' ' + cta_data['description']
|
||
|
||
if is_learning_mfe:
|
||
cta_data['event_data'] = {
|
||
'event_name': 'post_event',
|
||
'post_data': {
|
||
'body_params': {
|
||
'course_id': str(course_key),
|
||
},
|
||
'url': '{}{}'.format(settings.LMS_ROOT_URL, reverse('course-experience-reset-course-deadlines')),
|
||
},
|
||
'research_event_data': {
|
||
'block_id': str(xblock.location),
|
||
'location': f'{xblock.category}-view',
|
||
},
|
||
}
|
||
|
||
return cta_data
|