From 3c1de16eba0b43de17bd414589771d7c31ef3275 Mon Sep 17 00:00:00 2001
From: Dillon Dumesnil <ddumesnil@edx.org>
Date: Fri, 11 Dec 2020 19:18:23 +0000
Subject: [PATCH] AA-260/AA-500: Improvements to in course shift deadlines

As part of AA-500, we added a completeness check to showing the
banner since we didn't before. As part of AA-260, we now take into
account if a learner has more attempts left on a problem (regardless
of completeness) and allow them to shift their dates to try again.
---
 .../lib/xmodule/xmodule/css/capa/display.scss |  7 +++
 common/lib/xmodule/xmodule/vertical_block.py  |  6 +--
 lms/templates/problem.html                    |  9 ++--
 .../lib/xblock_services/call_to_action.py     |  4 +-
 .../call_to_action.py                         | 47 +++++++++++++++----
 5 files changed, 56 insertions(+), 17 deletions(-)

diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss
index e5a0a373a56..3270c6bcde3 100644
--- a/common/lib/xmodule/xmodule/css/capa/display.scss
+++ b/common/lib/xmodule/xmodule/css/capa/display.scss
@@ -1019,8 +1019,11 @@ div.problem .action {
     .submit-cta-description {
         color: $blue;
         font-size: small;
+        padding-right: $baseline / 2;
     }
     .submit-cta-link-button {
+        border: none;
+        padding-right: $baseline / 4;
         text-decoration: underline;
         text-transform: none;
     }
@@ -1035,6 +1038,10 @@ div.problem .action {
     font-size: $medium-font-size;
     -webkit-font-smoothing: antialiased;
     vertical-align: middle;
+
+    &.cta-enabled {
+      margin-top: 0;
+    }
   }
 }
 
diff --git a/common/lib/xmodule/xmodule/vertical_block.py b/common/lib/xmodule/xmodule/vertical_block.py
index 7912a6108fa..f5ca8988a0e 100644
--- a/common/lib/xmodule/xmodule/vertical_block.py
+++ b/common/lib/xmodule/xmodule/vertical_block.py
@@ -92,11 +92,11 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
                 'content': rendered_child.content
             })
 
-        cta_service = self.runtime.service(self, 'call_to_action')
-        vertical_banner_ctas = cta_service and cta_service.get_ctas(self, 'vertical_banner')
-
         completed = self.is_block_complete_for_assignments(completion_service)
         past_due = completed is False and self.due and self.due < datetime.now(pytz.UTC)
+        cta_service = self.runtime.service(self, 'call_to_action')
+        vertical_banner_ctas = (cta_service and cta_service.get_ctas(self, 'vertical_banner', completed)) or []
+
         fragment_context = {
             'items': contents,
             'xblock_context': context,
diff --git a/lms/templates/problem.html b/lms/templates/problem.html
index 3c6b5e17b5f..7ed8b4a16ea 100644
--- a/lms/templates/problem.html
+++ b/lms/templates/problem.html
@@ -1,6 +1,6 @@
 <%page expression_filter="h"/>
 <%!
-from django.utils.translation import ungettext, ugettext as _
+from django.utils.translation import ngettext, gettext as _
 from openedx.core.djangolib.markup import HTML
 %>
 
@@ -85,9 +85,10 @@ from openedx.core.djangolib.markup import HTML
             </form>
         % endif
       % endif
-      <div class="submission-feedback" id="submission_feedback_${short_id}">
-        % if attempts_allowed:
-          ${ungettext("You have used {num_used} of {num_total} attempt", "You have used {num_used} of {num_total} attempts", attempts_allowed).format(num_used=attempts_used, num_total=attempts_allowed)}
+      <div class="submission-feedback ${'cta-enabled' if submit_disabled_cta else ''}" id="submission_feedback_${short_id}">
+        ## When attempts are not 0, the CTA above will contain a message about the number of used attempts
+        % if attempts_allowed and (not submit_disabled_cta or attempts_used == 0):
+          ${ngettext("You have used {num_used} of {num_total} attempt", "You have used {num_used} of {num_total} attempts", attempts_allowed).format(num_used=attempts_used, num_total=attempts_allowed)}
         % endif
         <span class="sr">${_("Some problems have options such as save, reset, hints, or show answer. These options follow the Submit button.")}</span>
       </div>
diff --git a/openedx/core/lib/xblock_services/call_to_action.py b/openedx/core/lib/xblock_services/call_to_action.py
index 981ef4570a9..fb4910c2ee9 100644
--- a/openedx/core/lib/xblock_services/call_to_action.py
+++ b/openedx/core/lib/xblock_services/call_to_action.py
@@ -12,7 +12,7 @@ class CallToActionService(PluginManager):
     """
     NAMESPACE = 'openedx.call_to_action'
 
-    def get_ctas(self, xblock, category):
+    def get_ctas(self, xblock, category, completion=False):
         """
         Return the calls to action associated with the specified category for the given xblock.
 
@@ -45,5 +45,5 @@ class CallToActionService(PluginManager):
         """
         ctas = []
         for cta_provider in self.get_available_plugins().values():
-            ctas.extend(cta_provider().get_ctas(xblock, category))
+            ctas.extend(cta_provider().get_ctas(xblock, category, completion))
         return ctas
diff --git a/openedx/features/personalized_learner_schedules/call_to_action.py b/openedx/features/personalized_learner_schedules/call_to_action.py
index 13277c9a298..adaafcc7134 100644
--- a/openedx/features/personalized_learner_schedules/call_to_action.py
+++ b/openedx/features/personalized_learner_schedules/call_to_action.py
@@ -1,10 +1,14 @@
+"""
+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 gettext as _
+from django.utils.translation import ngettext, gettext as _
 
 from lms.djangoapps.course_home_api.utils import is_request_from_learning_mfe
 from openedx.core.lib.mobile_utils import is_request_from_mobile_app
@@ -14,12 +18,14 @@ 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):
+    def get_ctas(self, xblock, category, completed):
         """
         Return the calls to action associated with the specified category for the given xblock.
 
@@ -44,13 +50,13 @@ class PersonalizedLearnerScheduleCallToAction:
             # 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):
-                ctas.append(self._make_reset_deadlines_cta(xblock, is_learning_mfe))
+                ctas.append(self._make_reset_deadlines_cta(xblock, category, is_learning_mfe))
 
-        elif category == self.VERTICAL_BANNER:
+        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) for item in xblock.get_display_items()):
-                ctas.append(self._make_reset_deadlines_cta(xblock, is_learning_mfe))
+                ctas.append(self._make_reset_deadlines_cta(xblock, category, is_learning_mfe))
 
         return ctas
 
@@ -79,6 +85,10 @@ class PersonalizedLearnerScheduleCallToAction:
 
     @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
 
@@ -87,8 +97,11 @@ class PersonalizedLearnerScheduleCallToAction:
                     '%s.is_past_due into a method.', name)
         PersonalizedLearnerScheduleCallToAction.past_due_class_warnings.add(name)
 
-    @staticmethod
-    def _make_reset_deadlines_cta(xblock, is_learning_mfe=False):
+    @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
 
@@ -103,6 +116,24 @@ class PersonalizedLearnerScheduleCallToAction:
                              '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',
-- 
GitLab