From 53b22cbf1f9bd466151d8087ee19faab74a25381 Mon Sep 17 00:00:00 2001 From: Amir Qayyum Khan Date: Thu, 8 Oct 2015 12:41:35 +0500 Subject: [PATCH] Made dates configuration for CCX schedule same as in studio (Course) --- lms/djangoapps/ccx/tests/test_views.py | 37 ++++++-- lms/djangoapps/ccx/views.py | 91 +++++++++++++++---- lms/static/js/ccx/schedule.js | 77 ++++++++++++++-- .../sass/course/ccx_coach/_dashboard.scss | 48 ++++++++++ lms/templates/ccx/schedule.html | 58 ++++++------ lms/templates/ccx/schedule.underscore | 60 +++++++----- 6 files changed, 291 insertions(+), 80 deletions(-) diff --git a/lms/djangoapps/ccx/tests/test_views.py b/lms/djangoapps/ccx/tests/test_views.py index 4215485223..58b283bad5 100644 --- a/lms/djangoapps/ccx/tests/test_views.py +++ b/lms/djangoapps/ccx/tests/test_views.py @@ -52,6 +52,7 @@ from ccx_keys.locator import CCXLocator from lms.djangoapps.ccx.models import CustomCourseForEdX from lms.djangoapps.ccx.overrides import get_override_for_ccx, override_field_for_ccx from lms.djangoapps.ccx.tests.factories import CcxFactory +from lms.djangoapps.ccx.views import get_date def intercept_renderer(path, context): @@ -290,6 +291,21 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase): role = CourseCcxCoachRole(course_key) self.assertTrue(role.has_user(self.coach)) + def test_get_date(self): + """ + Assert that get_date returns valid date. + """ + ccx = self.make_ccx() + for section in self.course.get_children(): + self.assertEqual(get_date(ccx, section, 'start'), self.mooc_start) + self.assertEqual(get_date(ccx, section, 'due'), None) + for subsection in section.get_children(): + self.assertEqual(get_date(ccx, subsection, 'start'), self.mooc_start) + self.assertEqual(get_date(ccx, subsection, 'due'), self.mooc_due) + for unit in subsection.get_children(): + self.assertEqual(get_date(ccx, unit, 'start', parent_node=subsection), self.mooc_start) + self.assertEqual(get_date(ccx, unit, 'due', parent_node=subsection), self.mooc_due) + @SharedModuleStoreTestCase.modifies_courseware @patch('ccx.views.render_to_response', intercept_renderer) @patch('ccx.views.TODAY') @@ -341,15 +357,24 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase): kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}) response = self.client.get(url) schedule = json.loads(response.mako_context['schedule']) # pylint: disable=no-member + self.assertEqual(len(schedule), 2) self.assertEqual(schedule[0]['hidden'], False) - self.assertEqual(schedule[0]['start'], None) - self.assertEqual(schedule[0]['children'][0]['start'], None) - self.assertEqual(schedule[0]['due'], None) - self.assertEqual(schedule[0]['children'][0]['due'], None) + # If a coach does not override dates, then dates will be imported from master course. self.assertEqual( - schedule[0]['children'][0]['children'][0]['due'], None + schedule[0]['start'], + self.chapters[0].start.strftime('%Y-%m-%d %H:%M') ) + self.assertEqual( + schedule[0]['children'][0]['start'], + self.sequentials[0].start.strftime('%Y-%m-%d %H:%M') + ) + + if self.sequentials[0].due: + expected_due = self.sequentials[0].due.strftime('%Y-%m-%d %H:%M') + else: + expected_due = None + self.assertEqual(schedule[0]['children'][0]['due'], expected_due) url = reverse( 'save_ccx', @@ -392,7 +417,7 @@ class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase): # scheduled chapter ccx = CustomCourseForEdX.objects.get() course_start = get_override_for_ccx(ccx, self.course, 'start') - self.assertEqual(str(course_start)[:-9], u'2014-11-20 00:00') + self.assertEqual(str(course_start)[:-9], self.chapters[0].start.strftime('%Y-%m-%d %H:%M')) # Make sure grading policy adjusted policy = get_override_for_ccx(ccx, self.course, 'grading_policy', diff --git a/lms/djangoapps/ccx/views.py b/lms/djangoapps/ccx/views.py index d8362c3a33..d8db58e3d3 100644 --- a/lms/djangoapps/ccx/views.py +++ b/lms/djangoapps/ccx/views.py @@ -274,10 +274,16 @@ def save_ccx(request, course, ccx=None): ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'start_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'start') - due = parse_date(unit['due']) - if due: - override_field_for_ccx(ccx, block, 'due', due) + # Only subsection (aka sequential) and unit (aka vertical) have due dates. + if 'due' in unit: # checking that the key (due) exist in dict (unit). + due = parse_date(unit['due']) + if due: + override_field_for_ccx(ccx, block, 'due', due) + else: + ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) + clear_ccx_field_info_from_ccx_map(ccx, block, 'due') else: + # In case of section aka chapter we do not have due date. ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') @@ -398,6 +404,35 @@ def get_ccx_for_coach(course, coach): return None +def get_date(ccx, node, date_type=None, parent_node=None): + """ + This returns override or master date for section, subsection or a unit. + + :param ccx: ccx instance + :param node: chapter, subsection or unit + :param date_type: start or due + :param parent_node: parent of node + :return: start or due date + """ + date = get_override_for_ccx(ccx, node, date_type, None) + if date_type == "start": + master_date = node.start + else: + master_date = node.due + + if date is not None: + # Setting override date [start or due] + date = date.strftime('%Y-%m-%d %H:%M') + elif not parent_node and master_date is not None: + # Setting date from master course + date = master_date.strftime('%Y-%m-%d %H:%M') + elif parent_node is not None: + # Set parent date (vertical has same dates as subsections) + date = get_date(ccx, node=parent_node, date_type=date_type) + + return date + + def get_ccx_schedule(course, ccx): """ Generate a JSON serializable CCX schedule. @@ -409,28 +444,50 @@ def get_ccx_schedule(course, ccx): widgets, which use text inputs. Visits students visible nodes only; nodes children of hidden ones are skipped as well. + + Dates: + Only start date is applicable to a section. If ccx coach did not override start date then + getting it from the master course. + Both start and due dates are applicable to a subsection (aka sequential). If ccx coach did not override + these dates then getting these dates from corresponding subsection in master course. + Unit inherits start date and due date from its subsection. If ccx coach did not override these dates + then getting them from corresponding subsection in master course. """ for child in node.get_children(): # in case the children are visible to staff only, skip them if child.visible_to_staff_only: continue - start = get_override_for_ccx(ccx, child, 'start', None) - if start: - start = str(start)[:-9] - due = get_override_for_ccx(ccx, child, 'due', None) - if due: - due = str(due)[:-9] + hidden = get_override_for_ccx( ccx, child, 'visible_to_staff_only', child.visible_to_staff_only) - visited = { - 'location': str(child.location), - 'display_name': child.display_name, - 'category': child.category, - 'start': start, - 'due': due, - 'hidden': hidden, - } + + start = get_date(ccx, child, 'start') + if depth > 1: + # Subsection has both start and due dates and unit inherit dates from their subsections + if depth == 2: + due = get_date(ccx, child, 'due') + elif depth == 3: + # Get start and due date of subsection in case unit has not override dates. + due = get_date(ccx, child, 'due', node) + start = get_date(ccx, child, 'start', node) + + visited = { + 'location': str(child.location), + 'display_name': child.display_name, + 'category': child.category, + 'start': start, + 'due': due, + 'hidden': hidden, + } + else: + visited = { + 'location': str(child.location), + 'display_name': child.display_name, + 'category': child.category, + 'start': start, + 'hidden': hidden, + } if depth < 3: children = tuple(visit(child, depth + 1)) if children: diff --git a/lms/static/js/ccx/schedule.js b/lms/static/js/ccx/schedule.js index bc2d1d1cd3..eb643e45d9 100644 --- a/lms/static/js/ccx/schedule.js +++ b/lms/static/js/ccx/schedule.js @@ -49,6 +49,9 @@ var edx = edx || {}; self.render(); }); + // By default input date and time fileds are disable. + self.disableFields($('.ccx_due_date_time_fields')); + self.disableFields($('.ccx_start_date_time_fields')); // Add unit handlers this.chapter_select.on('change', function() { var chapter_location = self.chapter_select.val(); @@ -60,10 +63,15 @@ var edx = edx || {}; .append(self.schedule_options(chapter.children)); self.sequential_select.prop('disabled', false); $('#add-unit-button').prop('disabled', false); - self.set_datetime('start', chapter.start); - self.set_datetime('due', chapter.due); + // When a chapter is selected, start date fields are enabled and due date + // fields are disabled because due dates are not applicable on a chapter. + self.disableFields($('.ccx_due_date_time_fields')); + self.enableFields($('.ccx_start_date_time_fields')); } else { self.sequential_select.html('').prop('disabled', true); + // When no chapter is selected, all date fields are disabled. + self.disableFields($('.ccx_due_date_time_fields')); + self.disableFields($('.ccx_start_date_time_fields')); } }); @@ -78,8 +86,15 @@ var edx = edx || {}; self.vertical_select.prop('disabled', false); self.set_datetime('start', sequential.start); self.set_datetime('due', sequential.due); + // When a subsection (aka sequential) is selected, + // both start and due date fields are enabled. + self.enableFields($('.ccx_due_date_time_fields')); + self.enableFields($('.ccx_start_date_time_fields')); } else { + // When "All subsections" is selected, all date fields are disabled. self.vertical_select.html('').prop('disabled', true); + self.disableFields($('.ccx_due_date_time_fields')); + self.enableFields($('.ccx_start_date_time_fields')); } }); @@ -90,8 +105,16 @@ var edx = edx || {}; sequential = self.sequential_select.val(); var vertical = self.find_unit( self.hidden, chapter, sequential, vertical_location); - self.set_datetime('start', vertical.start); - self.set_datetime('due', vertical.due); + // When a unit (aka vertical) is selected, all date fields are disabled because units + // inherit dates from subsection + self.disableFields($('.ccx_due_date_time_fields')); + self.disableFields($('.ccx_start_date_time_fields')); + } else { + // When "All units" is selected, all date fields are enabled, + // because units inherit dates from subsections and we + // are showing dates from the selected subsection. + self.enableFields($('.ccx_due_date_time_fields')); + self.enableFields($('.ccx_start_date_time_fields')); } }); @@ -330,6 +353,14 @@ var edx = edx || {}; }); }, + disableFields: function($selector) { + $selector.find('select,input,button').prop('disabled', true); + }, + + enableFields: function($selector) { + $selector.find('select,input,button').prop('disabled', false); + }, + toggle_collapse: function(event) { event.preventDefault(); var row = $(this).closest('tr'); @@ -344,10 +375,19 @@ var edx = edx || {}; $(this).attr('aria-expanded', 'true'); $(this).find(".fa-caret-right").removeClass('fa-caret-right').addClass('fa-caret-down'); row.removeClass('collapsed').addClass('expanded'); - children.filter('.collapsed').each(function() { - children = children.not(self.get_children(this)); - }); - children.show(); + var depth = $(row).data('depth'); + var $childNodes = children.filter('.collapsed'); + if ($childNodes.length <= 0) { + children.show(); + } else { + // this will expand units. + $childNodes.each(function() { + var depthChild = $(this).data('depth'); + if (depth === (depthChild - 1)) { + $(this).show(); + } + }); + } } }, @@ -374,9 +414,9 @@ var edx = edx || {}; $(row).find('.ccx_sr_alert').attr('aria-expanded', 'false'); $(row).find('.fa-caret-down').removeClass('fa-caret-down').addClass('fa-caret-right'); row.removeClass('expanded').addClass('collapsed'); - $('table.ccx-schedule .sequential,.vertical').hide(); } }); + $('table.ccx-schedule .sequential,.vertical').hide(); }, enterNewDate: function(what) { @@ -429,8 +469,14 @@ var edx = edx || {}; } if (what === 'start') { unit.start = date + ' ' + time; + if (unit.category === "sequential") { + self.updateChildrenDates(unit, what, unit.start); + } } else { unit.due = date + ' ' + time; + if (unit.category === "sequential") { + self.updateChildrenDates(unit, what, unit.due); + } } modal.find('.close-modal').click(); self.dirty = true; @@ -440,6 +486,19 @@ var edx = edx || {}; }; }, + updateChildrenDates: function(sequential, date_type, date) { + // This code iterates the children (aka verticals) of a sequential. + // It updates start and due dates to corresponding dates + // of sequential (parent). + _.forEach(sequential.children, function (unit) { + if (date_type === 'start') { + unit.start = date; + } else { + unit.due = date; + } + }); + }, + find_unit: function(tree, chapter, sequential, vertical) { var units = self.find_lineage(tree, chapter, sequential, vertical); return units[units.length -1]; diff --git a/lms/static/sass/course/ccx_coach/_dashboard.scss b/lms/static/sass/course/ccx_coach/_dashboard.scss index 282f01e4a4..67a4233250 100644 --- a/lms/static/sass/course/ccx_coach/_dashboard.scss +++ b/lms/static/sass/course/ccx_coach/_dashboard.scss @@ -18,6 +18,11 @@ table.ccx-schedule { th, td { padding: 10px; } + td.no-link { + font-size: 13px; + text-shadow: 0 1px 0 #fcfbfb; + text-decoration: none; + } .sequential .unit { padding-left: 25px; } @@ -40,6 +45,7 @@ table.ccx-schedule { margin-left: 20px; } + .ccx-sidebar-panel { border: 1px solid #cbcbcb; padding: 15px; @@ -48,8 +54,46 @@ table.ccx-schedule { form.ccx-form { line-height: 1.5; + // inspiration was taken from https://github.com/edx/ux-pattern-library select { + @include font-size(16); + background: #fcfcfc; + border: 1px solid #e9e8e8; + box-sizing: padding-box; + color: #282c2e; + display: inline-block; + font-size: ($baseline*.9.5); + height: 40px; + line-height: 20px; + padding: 10px; + transition: all 125ms ease-in-out 0s; width: 100%; + &:disabled { + border-color: #cfd8dc; + background: #e7ecee; + cursor: not-allowed; + } + } + input { + @include font-size(15); + background: #FCFCFC none repeat scroll 0% 0%; + border: 1px solid #E7E6E6; + box-sizing: border-box; + color: #34383A; + display: inline-block; + line-height: normal; + transition: all 0.125s ease-in-out 0s; + padding: 5px 10px 5px 10px; + &:focus { + border-color: #0ea6ec; + color: #282c2e; + outline: 0; + } + &:disabled { + border-color: #cfd8dc; + background: #e7ecee; + cursor: not-allowed; + } } .field { margin: 5px 0 5px 0; @@ -72,6 +116,10 @@ button.ccx-button-link { } &:hover { color: brown; + background: none; + } + &:focus { + background: none; } } diff --git a/lms/templates/ccx/schedule.html b/lms/templates/ccx/schedule.html index 0c52ddbbeb..6950950c9d 100644 --- a/lms/templates/ccx/schedule.html +++ b/lms/templates/ccx/schedule.html @@ -39,14 +39,14 @@

-
+
## Translators: This explains to people using a screen reader how to interpret the format of YYYY-MM-DD - + ## Translators: This explains to people using a screen reader how to interpret the format of HH:MM - +
@@ -63,7 +63,7 @@

${_("You have unsaved changes.")}


- +
@@ -87,31 +87,35 @@ -
- - - ## Translators: This explains to people using a screen reader how to interpret the format of HH:MM - - -
-
-