From eb02f2adc1c5da99e21d63daae6cef07dcf98368 Mon Sep 17 00:00:00 2001
From: Nimisha Asthagiri <nasthagiri@edx.org>
Date: Mon, 11 Jul 2016 18:15:16 -0400
Subject: [PATCH] LMS Update to hide subsection content based on due-date

TNL-4905
---
 common/lib/xmodule/xmodule/seq_module.py          | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------------------------------------------
 common/lib/xmodule/xmodule/tests/test_sequence.py |  80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
 lms/djangoapps/courseware/views/index.py          |   3 ++-
 lms/templates/hidden_content.html                 |  26 ++++++++++++++++++++++++++
 lms/templates/seq_module.html                     |   4 ++--
 5 files changed, 233 insertions(+), 90 deletions(-)
 create mode 100644 lms/templates/hidden_content.html

diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index abe377b..11933b0 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -4,6 +4,8 @@ xModule implementation of a learning sequence
 
 # pylint: disable=abstract-method
 import collections
+from datetime import datetime
+from django.utils.timezone import UTC
 import json
 import logging
 from pkg_resources import resource_string
@@ -38,13 +40,22 @@ class SequenceFields(object):
     # NOTE: Position is 1-indexed.  This is silly, but there are now student
     # positions saved on prod, so it's not easy to fix.
     position = Integer(help="Last tab viewed in this sequence", scope=Scope.user_state)
+
     due = Date(
         display_name=_("Due Date"),
         help=_("Enter the date by which problems are due."),
         scope=Scope.settings,
     )
 
-    # Entrance Exam flag -- see cms/contentstore/views/entrance_exam.py for usage
+    hide_after_due = Boolean(
+        display_name=_("Hide sequence content After Due Date"),
+        help=_(
+            "If set, the sequence content is hidden for non-staff users after the due date has passed."
+        ),
+        default=False,
+        scope=Scope.settings,
+    )
+
     is_entrance_exam = Boolean(
         display_name=_("Is Entrance Exam"),
         help=_(
@@ -97,16 +108,6 @@ class ProctoringFields(object):
         scope=Scope.settings,
     )
 
-    hide_after_due = Boolean(
-        display_name=_("Hide Exam Results After Due Date"),
-        help=_(
-            "This setting overrides the default behavior of showing exam results after the due date has passed."
-            " Currently only supported for timed exams."
-        ),
-        default=False,
-        scope=Scope.settings,
-    )
-
     is_practice_exam = Boolean(
         display_name=_("Is Practice Exam"),
         help=_(
@@ -177,90 +178,157 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
 
         raise NotFoundError('Unexpected dispatch type')
 
+    @classmethod
+    def verify_current_content_visibility(cls, due, hide_after_due):
+        """
+        Returns whether the content visibility policy passes
+        for the given due date and hide_after_due values and
+        the current date-time.
+        """
+        return (
+            not due or
+            not hide_after_due or
+            datetime.now(UTC()) < due
+        )
+
     def student_view(self, context):
+        context = context or {}
+        self._capture_basic_metrics()
+        banner_text = None
+        special_html_view = self._hidden_content_student_view(context) or self._special_exam_student_view()
+        if special_html_view:
+            masquerading_as_specific_student = context.get('specific_masquerade', False)
+            banner_text, special_html = special_html_view
+            if special_html and not masquerading_as_specific_student:
+                return Fragment(special_html)
+        return self._student_view(context, banner_text)
+
+    def _special_exam_student_view(self):
+        """
+        Checks whether this sequential is a special exam.  If so, returns
+        a banner_text or the fragment to display depending on whether
+        staff is masquerading.
+        """
+        if self.is_time_limited:
+            special_exam_html = self._time_limited_student_view()
+            if special_exam_html:
+                banner_text = _("This exam is hidden from the learner.")
+                return banner_text, special_exam_html
+
+    def _hidden_content_student_view(self, context):
+        """
+        Checks whether the content of this sequential is hidden from the
+        runtime user. If so, returns a banner_text or the fragment to
+        display depending on whether staff is masquerading.
+        """
+        if not self._can_user_view_content():
+            subsection_format = (self.format or _("subsection")).lower()  # pylint: disable=no-member
+
+            # Translators: subsection_format refers to the assignment
+            # type of the subsection, such as Homework, Lab, Exam, etc.
+            banner_text = _(
+                "Because the due date has passed, "
+                "this {subsection_format} is hidden from the learner."
+            ).format(subsection_format=subsection_format)
+
+            hidden_content_html = self.system.render_template(
+                'hidden_content.html',
+                {
+                    'subsection_format': subsection_format,
+                    'progress_url': context.get('progress_url'),
+                }
+            )
+
+            return banner_text, hidden_content_html
+
+    def _can_user_view_content(self):
+        """
+        Returns whether the runtime user can view the content
+        of this sequential.
+        """
+        return (
+            self.runtime.user_is_staff or
+            self.verify_current_content_visibility(self.due, self.hide_after_due)
+        )
+
+    def _student_view(self, context, banner_text=None):
+        """
+        Returns the rendered student view of the content of this
+        sequential.  If banner_text is given, it is added to the
+        content.
+        """
         display_items = self.get_display_items()
+        self._update_position(context, len(display_items))
 
+        fragment = Fragment()
+        params = {
+            'items': self._render_student_view_for_items(context, display_items, fragment),
+            'element_id': self.location.html_id(),
+            'item_id': self.location.to_deprecated_string(),
+            'position': self.position,
+            'tag': self.location.category,
+            'ajax_url': self.system.ajax_url,
+            'next_url': context.get('next_url'),
+            'prev_url': context.get('prev_url'),
+            'banner_text': banner_text,
+        }
+        fragment.add_content(self.system.render_template("seq_module.html", params))
+
+        self._capture_full_seq_item_metrics(display_items)
+        self._capture_current_unit_metrics(display_items)
+
+        return fragment
+
+    def _update_position(self, context, number_of_display_items):
+        """
+        Update the user's sequential position given the context and the
+        number_of_display_items
+        """
         # If we're rendering this sequence, but no position is set yet,
         # or exceeds the length of the displayable items,
         # default the position to the first element
         if context.get('requested_child') == 'first':
             self.position = 1
         elif context.get('requested_child') == 'last':
-            self.position = len(display_items) or 1
-        elif self.position is None or self.position > len(display_items):
+            self.position = number_of_display_items or 1
+        elif self.position is None or self.position > number_of_display_items:
             self.position = 1
 
-        ## Returns a set of all types of all sub-children
-        contents = []
-
-        fragment = Fragment()
-        context = context or {}
-
+    def _render_student_view_for_items(self, context, display_items, fragment):
+        """
+        Updates the given fragment with rendered student views of the given
+        display_items.  Returns a list of dict objects with information about
+        the given display_items.
+        """
         bookmarks_service = self.runtime.service(self, "bookmarks")
         context["username"] = self.runtime.service(self, "user").get_current_user().opt_attrs['edx-platform.username']
-
-        parent_module = self.get_parent()
         display_names = [
-            parent_module.display_name_with_default,
+            self.get_parent().display_name_with_default,
             self.display_name_with_default
         ]
-
-        # We do this up here because proctored exam functionality could bypass
-        # rendering after this section.
-        self._capture_basic_metrics()
-
-        # Is this sequential part of a timed or proctored exam?
-        masquerading = context.get('specific_masquerade', False)
-        special_exam_html = None
-        if self.is_time_limited:
-            special_exam_html = self._time_limited_student_view(context)
-
-            # Do we have an applicable alternate rendering
-            # from the edx_proctoring subsystem?
-            if special_exam_html and not masquerading:
-                fragment.add_content(special_exam_html)
-                return fragment
-
-        for child in display_items:
-            is_bookmarked = bookmarks_service.is_bookmarked(usage_key=child.scope_ids.usage_id)
+        contents = []
+        for item in display_items:
+            is_bookmarked = bookmarks_service.is_bookmarked(usage_key=item.scope_ids.usage_id)
             context["bookmarked"] = is_bookmarked
 
-            progress = child.get_progress()
-            rendered_child = child.render(STUDENT_VIEW, context)
-            fragment.add_frag_resources(rendered_child)
+            progress = item.get_progress()
+            rendered_item = item.render(STUDENT_VIEW, context)
+            fragment.add_frag_resources(rendered_item)
 
-            childinfo = {
-                'content': rendered_child.content,
-                'page_title': getattr(child, 'tooltip_title', ''),
+            iteminfo = {
+                'content': rendered_item.content,
+                'page_title': getattr(item, 'tooltip_title', ''),
                 'progress_status': Progress.to_js_status_str(progress),
                 'progress_detail': Progress.to_js_detail_str(progress),
-                'type': child.get_icon_class(),
-                'id': child.scope_ids.usage_id.to_deprecated_string(),
+                'type': item.get_icon_class(),
+                'id': item.scope_ids.usage_id.to_deprecated_string(),
                 'bookmarked': is_bookmarked,
-                'path': " > ".join(display_names + [child.display_name_with_default]),
+                'path': " > ".join(display_names + [item.display_name_with_default]),
             }
 
-            contents.append(childinfo)
+            contents.append(iteminfo)
 
-        params = {
-            'items': contents,
-            'element_id': self.location.html_id(),
-            'item_id': self.location.to_deprecated_string(),
-            'position': self.position,
-            'tag': self.location.category,
-            'ajax_url': self.system.ajax_url,
-            'next_url': context.get('next_url'),
-            'prev_url': context.get('prev_url'),
-            'override_hidden_exam': masquerading and special_exam_html is not None,
-        }
-
-        fragment.add_content(self.system.render_template("seq_module.html", params))
-
-        self._capture_full_seq_item_metrics(display_items)
-        self._capture_current_unit_metrics(display_items)
-
-        # Get all descendant XBlock types and counts
-        return fragment
+        return contents
 
     def _locations_in_subtree(self, node):
         """
@@ -328,7 +396,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
             for block_type, count in curr_block_counts.items():
                 newrelic.agent.add_custom_parameter('seq.current.block_counts.{}'.format(block_type), count)
 
-    def _time_limited_student_view(self, context):
+    def _time_limited_student_view(self):
         """
         Delegated rendering of a student view when in a time
         limited view. This ultimately calls down into edx_proctoring
diff --git a/common/lib/xmodule/xmodule/tests/test_sequence.py b/common/lib/xmodule/xmodule/tests/test_sequence.py
index fedb839..f98319b 100644
--- a/common/lib/xmodule/xmodule/tests/test_sequence.py
+++ b/common/lib/xmodule/xmodule/tests/test_sequence.py
@@ -2,6 +2,10 @@
 Tests for sequence module.
 """
 # pylint: disable=no-member
+from datetime import timedelta
+import ddt
+from django.utils.timezone import now
+from freezegun import freeze_time
 from mock import Mock
 from xblock.reference.user_service import XBlockUser, UserService
 from xmodule.tests import get_test_system
@@ -24,10 +28,15 @@ class StubUserService(UserService):
         return user
 
 
+@ddt.ddt
 class SequenceBlockTestCase(XModuleXmlImportTest):
     """
     Tests for the Sequence Module.
     """
+    TODAY = now()
+    TOMORROW = TODAY + timedelta(days=1)
+    DAY_AFTER_TOMORROW = TOMORROW + timedelta(days=1)
+
     @classmethod
     def setUpClass(cls):
         super(SequenceBlockTestCase, cls).setUpClass()
@@ -54,13 +63,16 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
         chapter_1 = xml.ChapterFactory.build(parent=course)  # has 2 child sequences
         xml.ChapterFactory.build(parent=course)  # has 0 child sequences
         chapter_3 = xml.ChapterFactory.build(parent=course)  # has 1 child sequence
-        chapter_4 = xml.ChapterFactory.build(parent=course)  # has 2 child sequences
+        chapter_4 = xml.ChapterFactory.build(parent=course)  # has 1 child sequence, with hide_after_due
 
         xml.SequenceFactory.build(parent=chapter_1)
         xml.SequenceFactory.build(parent=chapter_1)
         sequence_3_1 = xml.SequenceFactory.build(parent=chapter_3)  # has 3 verticals
-        xml.SequenceFactory.build(parent=chapter_4)
-        xml.SequenceFactory.build(parent=chapter_4)
+        xml.SequenceFactory.build(  # sequence_4_1
+            parent=chapter_4,
+            hide_after_due=str(True),
+            due=str(cls.TOMORROW),
+        )
 
         for _ in range(3):
             xml.VerticalFactory.build(parent=sequence_3_1)
@@ -98,9 +110,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
     def test_render_student_view(self):
         html = self._get_rendered_student_view(
             self.sequence_3_1,
-            requested_child=None,
-            next_url='NextSequential',
-            prev_url='PrevSequential'
+            extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
         )
         self._assert_view_at_position(html, expected_position=1)
         self.assertIn(unicode(self.sequence_3_1.location), html)
@@ -115,20 +125,15 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
         html = self._get_rendered_student_view(self.sequence_3_1, requested_child='last')
         self._assert_view_at_position(html, expected_position=3)
 
-    def _get_rendered_student_view(self, sequence, requested_child, next_url=None, prev_url=None):
+    def _get_rendered_student_view(self, sequence, requested_child=None, extra_context=None):
         """
         Returns the rendered student view for the given sequence and the
         requested_child parameter.
         """
-        return sequence.xmodule_runtime.render(
-            sequence,
-            STUDENT_VIEW,
-            {
-                'requested_child': requested_child,
-                'next_url': next_url,
-                'prev_url': prev_url,
-            },
-        ).content
+        context = {'requested_child': requested_child}
+        if extra_context:
+            context.update(extra_context)
+        return sequence.xmodule_runtime.render(sequence, STUDENT_VIEW, context).content
 
     def _assert_view_at_position(self, rendered_html, expected_position):
         """
@@ -140,3 +145,46 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
         html = self._get_rendered_student_view(self.sequence_3_1, requested_child=None)
         for child in self.sequence_3_1.children:
             self.assertIn("'page_title': '{}'".format(child.name), html)
+
+    def test_hidden_content_before_due(self):
+        html = self._get_rendered_student_view(self.sequence_4_1)
+        self.assertIn("seq_module.html", html)
+        self.assertIn("'banner_text': None", html)
+
+    @freeze_time(DAY_AFTER_TOMORROW)
+    @ddt.data(
+        (None, 'subsection'),
+        ('Homework', 'homework'),
+    )
+    @ddt.unpack
+    def test_hidden_content_past_due(self, format_type, expected_text):
+        progress_url = 'http://test_progress_link'
+        self._set_sequence_format(self.sequence_4_1, format_type)
+        html = self._get_rendered_student_view(
+            self.sequence_4_1,
+            extra_context=dict(progress_url=progress_url),
+        )
+        self.assertIn("hidden_content.html", html)
+        self.assertIn(progress_url, html)
+        self.assertIn("'subsection_format': '{}'".format(expected_text), html)
+
+    @freeze_time(DAY_AFTER_TOMORROW)
+    def test_masquerade_hidden_content_past_due(self):
+        self._set_sequence_format(self.sequence_4_1, "Homework")
+        html = self._get_rendered_student_view(
+            self.sequence_4_1,
+            extra_context=dict(specific_masquerade=True),
+        )
+        self.assertIn("seq_module.html", html)
+        self.assertIn(
+            "'banner_text': 'Because the due date has passed, "
+            "this homework is hidden from the learner.'",
+            html
+        )
+
+    def _set_sequence_format(self, sequence, format_type):
+        """
+        Sets the format field on the given sequence to the
+        given value.
+        """
+        sequence._xmodule.format = format_type  # pylint: disable=protected-access
diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py
index a361898..8fcb0f8 100644
--- a/lms/djangoapps/courseware/views/index.py
+++ b/lms/djangoapps/courseware/views/index.py
@@ -447,7 +447,7 @@ class CoursewareIndex(View):
             return "{url}?child={requested_child}".format(
                 url=reverse(
                     'courseware_section',
-                    args=[unicode(self.course.id), section_info['chapter_url_name'], section_info['url_name']],
+                    args=[unicode(self.course_key), section_info['chapter_url_name'], section_info['url_name']],
                 ),
                 requested_child=requested_child,
             )
@@ -455,6 +455,7 @@ class CoursewareIndex(View):
         section_context = {
             'activate_block_id': self.request.GET.get('activate_block_id'),
             'requested_child': self.request.GET.get("child"),
+            'progress_url': reverse('progress', kwargs={'course_id': unicode(self.course_key)}),
         }
         if previous_of_active_section:
             section_context['prev_url'] = _compute_section_url(previous_of_active_section, 'last')
diff --git a/lms/templates/hidden_content.html b/lms/templates/hidden_content.html
new file mode 100644
index 0000000..7e6f4b5
--- /dev/null
+++ b/lms/templates/hidden_content.html
@@ -0,0 +1,26 @@
+<%page expression_filter="h"/>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.markup import HTML, Text
+%>
+
+<div class="sequence hidden-content proctored-exam completed">
+  <h3>
+      ${_("The due date for this {subsection_format} has passed.").format(
+        subsection_format=subsection_format,
+      )}
+  </h3>
+  <hr>
+  <p>
+      ${Text(_(
+          "Because the due date has passed, this {subsection_format} "
+          "is no longer available.{line_break}If you have completed this {subsection_format}, "
+          "your grade is available on the {link_start}progress page{link_end}."
+        )).format(
+          subsection_format=subsection_format,
+          line_break=HTML("<br>"),
+          link_start=HTML("<a href='{}'>").format(progress_url),
+          link_end=HTML("</a>"),
+      )}
+  </p>
+</div>
diff --git a/lms/templates/seq_module.html b/lms/templates/seq_module.html
index 7996ab7..6475416 100644
--- a/lms/templates/seq_module.html
+++ b/lms/templates/seq_module.html
@@ -3,12 +3,12 @@
 
 <div id="sequence_${element_id}" class="sequence" data-id="${item_id}" data-position="${position}" data-ajax-url="${ajax_url}" data-next-url="${next_url}" data-prev-url="${prev_url}">
   <div class="path"></div>
-    % if override_hidden_exam:
+    % if banner_text:
       <div class="pattern-library-shim alert alert-information subsection-header" tabindex="-1">
         <span class="pattern-library-shim icon alert-icon icon-bullhorn" aria-hidden="true"></span>
         <div class="pattern-library-shim alert-message">
           <p class="pattern-library-shim alert-copy">
-            ${_("This exam is hidden from the learner.")}
+            ${banner_text}
           </p>
         </div>
       </div>
--
libgit2 0.26.0