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 @@