From 92c816966db64e3dcbe11445abfdbc5b4bc22aa3 Mon Sep 17 00:00:00 2001
From: Michael Roytman <mroytman@edx.org>
Date: Tue, 26 Jun 2018 15:11:21 -0400
Subject: [PATCH] change course_validation endpoint to support option to get
 data for only graded assignments and add ability to scroll to elements on the
 course outline page

---
 .../contentstore/api/tests/test_validation.py |  8 +--
 .../api/views/course_validation.py            | 59 +++++++++++++------
 cms/static/js/views/pages/course_outline.js   | 38 ++++++++++++
 cms/templates/js/course-outline.underscore    |  2 +-
 4 files changed, 85 insertions(+), 22 deletions(-)

diff --git a/cms/djangoapps/contentstore/api/tests/test_validation.py b/cms/djangoapps/contentstore/api/tests/test_validation.py
index acaa4dc0c68..87142cb3bec 100644
--- a/cms/djangoapps/contentstore/api/tests/test_validation.py
+++ b/cms/djangoapps/contentstore/api/tests/test_validation.py
@@ -77,11 +77,10 @@ class CourseValidationViewTest(SharedModuleStoreTestCase, APITestCase):
         self.assertEqual(resp.status_code, status.HTTP_200_OK)
         expected_data = {
             'assignments': {
-                'num_with_dates_before_end': 0,
-                'num_with_dates': 0,
-                'total_visible': 1,
-                'num_with_dates_after_start': 0,
                 'total_number': 1,
+                'total_visible': 1,
+                'assignments_with_dates_before_start': [],
+                'assignments_with_dates_after_end': [],
             },
             'dates': {
                 'has_start_date': True,
@@ -99,4 +98,5 @@ class CourseValidationViewTest(SharedModuleStoreTestCase, APITestCase):
             },
             'is_self_paced': True,
         }
+
         self.assertDictEqual(resp.data, expected_data)
diff --git a/cms/djangoapps/contentstore/api/views/course_validation.py b/cms/djangoapps/contentstore/api/views/course_validation.py
index f6c68e9e17c..b0b7ee35329 100644
--- a/cms/djangoapps/contentstore/api/views/course_validation.py
+++ b/cms/djangoapps/contentstore/api/views/course_validation.py
@@ -33,6 +33,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
         * grades
         * certificates
         * updates
+        * graded_only (boolean) - whether to included graded subsections only in the assignments information.
 
     **GET Response Values**
 
@@ -45,9 +46,8 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
         * assignments
             * total_number - total number of assignments in the course.
             * total_visible - number of assignments visible to learners in the course.
-            * num_with_dates - number of assignments with due dates.
-            * num_with_dates_after_start - number of assignments with due dates after the start date.
-            * num_with_dates_before_end - number of assignments with due dates before the end date.
+            * assignments_with_dates_before_start - assignments with due dates before the start date.
+            * assignments_with_dates_after_end - assignments with due dates after the end date.
         * grades
             * sum_of_weights - sum of weights for all assignments in the course (valid ones should equal 1).
         * certificates
@@ -77,7 +77,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
                 )
             if get_bool_param(request, 'assignments', all_requested):
                 response.update(
-                    assignments=self._assignments_validation(course)
+                    assignments=self._assignments_validation(course, request)
                 )
             if get_bool_param(request, 'grades', all_requested):
                 response.update(
@@ -106,28 +106,54 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
             has_end_date=course.end is not None,
         )
 
-    def _assignments_validation(self, course):
+    def _assignments_validation(self, course, request):
         assignments, visible_assignments = self._get_assignments(course)
-        assignments_with_dates = [a for a in visible_assignments if a.due]
+        assignments_with_dates = [
+            a for a in visible_assignments if a.due
+        ]
 
-        num_with_dates = len(assignments_with_dates)
-        num_with_dates_after_start = (
-            len([a for a in assignments_with_dates if a.due > course.start])
+        assignments_with_dates_before_start = (
+            [
+                {'id': unicode(a.location), 'display_name': a.display_name}
+                for a in assignments_with_dates
+                if a.due < course.start
+            ]
             if self._has_start_date(course)
-            else 0
+            else []
         )
-        num_with_dates_before_end = (
-            len([a for a in assignments_with_dates if a.due < course.end])
+
+        assignments_with_dates_after_end = (
+            [
+                {'id': unicode(a.location), 'display_name': a.display_name}
+                for a in assignments_with_dates
+                if a.due > course.end
+            ]
             if course.end
-            else 0
+            else []
         )
 
+        if get_bool_param(request, 'graded_only', False):
+            assignments_with_dates = [
+                a
+                for a in visible_assignments
+                if a.due and a.graded
+            ]
+
+            assignments_with_dates_before_start = [
+                {'id': unicode(a.location), 'display_name': a.display_name}
+                for a in assignments_with_dates if a.due < course.start
+            ]
+
+            assignments_with_dates_after_end = [
+                {'id': unicode(a.location), 'display_name': a.display_name}
+                for a in assignments_with_dates if a.due > course.end
+            ]
+
         return dict(
             total_number=len(assignments),
             total_visible=len(visible_assignments),
-            num_with_dates=num_with_dates,
-            num_with_dates_after_start=num_with_dates_after_start,
-            num_with_dates_before_end=num_with_dates_before_end,
+            assignments_with_dates_before_start=assignments_with_dates_before_start,
+            assignments_with_dates_after_end=assignments_with_dates_after_end,
         )
 
     def _grades_validation(self, course):
@@ -158,7 +184,6 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
             for section in sections
             for assignment_usage_key in section.children
         ]
-
         visible_sections = [
             s for s in sections
             if not s.visible_to_staff_only and not s.hide_from_toc
diff --git a/cms/static/js/views/pages/course_outline.js b/cms/static/js/views/pages/course_outline.js
index 7e65dd8e3d4..00335e92a39 100644
--- a/cms/static/js/views/pages/course_outline.js
+++ b/cms/static/js/views/pages/course_outline.js
@@ -18,6 +18,18 @@ define([
                 'click .button-toggle-expand-collapse': 'toggleExpandCollapse'
             },
 
+            /**
+             * keep a running timeout counter of 5,000 milliseconds
+             * for finding an element; see afterRender and scrollToElement function
+             */
+            findElementPollingTimeout: 5000,
+
+            /**
+             * used as the delay parameter to setTimeout in scrollToElement
+             * function for polling for an element
+             */
+            pollingDelay: 100,
+
             options: {
                 collapsedClass: 'is-collapsed'
             },
@@ -90,6 +102,32 @@ define([
                 return $.Deferred().resolve().promise();
             },
 
+            afterRender: function() {
+                this.scrollToElement();
+            },
+
+            /**
+             * recursively poll for element specified by the URL fragment
+             * at 100 millisecond intervals until element is found or
+             * Polling is reached
+             */
+            scrollToElement: function () {
+                this.findElementPollingTimeout -= this.pollingDelay;
+
+                const elementID = window.location.hash.replace("#", "");
+
+                if (this.findElementPollingTimeout > 0) {
+                    if (elementID) {
+                        const element = document.getElementById(elementID);
+                        if (element) {
+                            element.scrollIntoView();
+                        } else {
+                            setTimeout(this.scrollToElement, this.pollingDelay);
+                        }
+                    }
+                }
+            },
+
             hasContent: function() {
                 return this.model.hasChildren();
             },
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore
index 5599a001812..13aadc7c2e5 100644
--- a/cms/templates/js/course-outline.underscore
+++ b/cms/templates/js/course-outline.underscore
@@ -97,7 +97,7 @@ if (is_proctored_exam) {
 %>
 <% if (parentInfo) { %>
 <li class="outline-item outline-<%- xblockType %> <%- visibilityClass %> is-draggable <%- includesChildren ? 'is-collapsible' : '' %> <%- isCollapsed ? 'is-collapsed' : '' %>"
-    data-parent="<%- parentInfo.get('id') %>" data-locator="<%- xblockInfo.get('id') %>">
+    data-parent="<%- parentInfo.get('id') %>" data-locator="<%- xblockInfo.get('id') %>" id="<%- xblockInfo.get('id') %>">
 
     <span class="draggable-drop-indicator draggable-drop-indicator-before"><span class="icon fa fa-caret-right" aria-hidden="true"></span></span>
     <% if (xblockInfo.isHeaderVisible()) { %>
-- 
GitLab