Commit 53b22cbf by Amir Qayyum Khan

Made dates configuration for CCX schedule same as in studio (Course)

parent 8102bd51
......@@ -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',
......
......@@ -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:
......
......@@ -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];
......
......@@ -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;
}
}
......
......@@ -39,14 +39,14 @@
<header>
<h2 id="ccx_schedule_set_date_heading"></h2>
</header>
<form>
<form class="ccx-form">
<div class="field datepair">
## Translators: This explains to people using a screen reader how to interpret the format of YYYY-MM-DD
<label class="sr form-label" for="ccx_dialog_date">${_('Date format four digit year dash two digit month dash two digit day')}</label>
<input placeholder="Date" class="date" type="text" name="date" id="ccx_dialog_date" size="11" />
<input placeholder="${_('Date')}" class="date" type="text" name="date" id="ccx_dialog_date" size="11" />
## Translators: This explains to people using a screen reader how to interpret the format of HH:MM
<label class="sr form-label" for="ccx_dialog_time">${_('Time format two digit hours colon two digit minutes')}</label>
<input placeholder="Time" class="time" type="text" name="time" id="ccx_dialog_time" size="6" />
<input placeholder="${_('Time')}" class="time" type="text" name="time" id="ccx_dialog_time" size="6" />
</div>
<div class="field">
<button type="submit" class="btn btn-primary">${_('Set date')}</button>
......@@ -63,7 +63,7 @@
<p id="message_save" class="text-helper">${_("You have unsaved changes.")}</p>
<div class="field">
<br/>
<button id="save-changes" aria-describedby="message_save">${_("Save changes")}</button>
<button id="save-changes" aria-describedby="message_save" class="ccx-schedule-save-changes">${_("Save changes")}</button>
</div>
</form>
</div>
......@@ -87,31 +87,35 @@
<label for="ccx_vertical" class="form-label"><b>${_('Unit')}</b></label>
<select name="vertical" id="ccx_vertical"></select>
</div>
<div class="field datepair">
<label for="ccx_start_date" class="form-label">
<b>${_('Start Date')}</b>
<span class="sr">
## Translators: This explains to people using a screen reader how to interpret the format of YYYY-MM-DD
&nbsp;${_('format four digit year dash two digit month dash two digit day')}
</span>
</label>
<input placeholder="yyyy-mm-dd" type="text" class="date" name="start_date" id="ccx_start_date" />
## Translators: This explains to people using a screen reader how to interpret the format of HH:MM
<label for="ccx_start_time" class="sr form-label">${_('Start time format two digit hours colon two digit minutes')}</label>
<input placeholder="time" type="text" class="time" name="start_time" id="ccx_start_time"/>
</div>
<div class="field datepair">
<label for="ccx_due_date" class="form-label">
<b>${_('Due Date')}</b> ${_('(Optional)')}
<span class="sr">
<div class="ccx_start_date_time_fields">
<div class="field datepair">
<label for="ccx_start_date" class="form-label">
<b>${_('Start Date')}</b>
<span class="sr">
## Translators: This explains to people using a screen reader how to interpret the format of YYYY-MM-DD
&nbsp;${_('format four digit year dash two digit month dash two digit day')}
</span>
</label>
<input placeholder="yyyy-mm-dd" type="text" class="date" name="due_date" id="ccx_due_date"/>
## Translators: This explains to people using a screen reader how to interpret the format of HH:MM
<label for="ccx_due_time" class="sr form-label">${_('Due Time format two digit hours colon two digit minutes')}</label>
<input placeholder="time" type="text" class="time" name="due_time" id="ccx_due_time"/>
</span>
</label>
<input placeholder="${_('yyyy-mm-dd')}" type="text" class="date" name="start_date" id="ccx_start_date" />
## Translators: This explains to people using a screen reader how to interpret the format of HH:MM
<label for="ccx_start_time" class="sr form-label">${_('Start time format two digit hours colon two digit minutes')}</label>
<input placeholder="${_('time')}" type="text" class="time" name="start_time" id="ccx_start_time"/>
</div>
</div>
<div class="ccx_due_date_time_fields">
<div class="field datepair">
<label for="ccx_due_date" class="form-label">
<b>${_('Due Date')}</b> ${_('(Optional)')}
<span class="sr">
## Translators: This explains to people using a screen reader how to interpret the format of YYYY-MM-DD
&nbsp;${_('format four digit year dash two digit month dash two digit day')}
</span>
</label>
<input placeholder="${_('yyyy-mm-dd')}" type="text" class="date" name="due_date" id="ccx_due_date"/>
## Translators: This explains to people using a screen reader how to interpret the format of HH:MM
<label for="ccx_due_time" class="sr form-label">${_('Due Time format two digit hours colon two digit minutes')}</label>
<input placeholder="${_('time')}" type="text" class="time" name="due_time" id="ccx_due_time"/>
</div>
</div>
<div class="field">
<br/>
......
......@@ -24,9 +24,13 @@
<td class="unit">
<button class="toggle-collapse ccx-button-link" aria-expanded="false">
<i class="fa fa-caret-right"></i>
<span class="sr"><%- gettext('toggle chapter') %>&nbsp;<%= chapter.display_name %></span>
<span class="sr">
<%- interpolate(gettext('toggle chapter %(displayName)s'),
{displayName: chapter.display_name}, true) %>
</span>
</button>
<span class="sr"><%- gettext('Section') %>&nbsp;</span><%= chapter.display_name %>
<span class="sr">
<%- gettext('Section') %>&nbsp;</span><%= chapter.display_name %>
</td>
<td class="date start-date">
<button class="ccx-button-link">
......@@ -34,13 +38,11 @@
<span class="sr"><%- gettext('Click to change') %></span>
</button>
</td>
<td class="date due-date">
<button class="ccx-button-link">
<%= chapter.due %>
<span class="sr"><%- gettext('Click to change') %></span>
</button>
<td class="date due-date no-link">
<%- gettext('N/A') %>
</td>
<td><button class="remove-unit ccx-button-link" aria-label="Remove chapter <%= chapter.display_name %>">
<td><button class="remove-unit ccx-button-link" aria-label="<%- interpolate(
gettext('Remove chapter %(chapterDisplayName)s'), {chapterDisplayName: chapter.display_name}, true) %>">
<i class="fa fa-remove" aria-hidden="true"></i> <%- gettext('remove') %>
</button></td>
</tr>
......@@ -50,7 +52,10 @@
<td class="unit">
<button class="toggle-collapse ccx-button-link" aria-expanded="false">
<i class="fa fa-caret-right"></i>
<span class="sr"><%- gettext('toggle subsection') %>&nbsp;<%= child.display_name %></span>
<span class="sr">
<%- interpolate(gettext('toggle subsection %(displayName)s'),
{displayName: child.display_name}, true) %>
</span>
</button>
<span class="sr"><%- gettext('Subsection') %>&nbsp;</span><%= child.display_name %>
</td>
......@@ -66,32 +71,45 @@
<span class="sr"><%- gettext('Click to change') %></span>
</button>
</td>
<td><button class="remove-unit ccx-button-link" aria-label="Remove subsection <%= child.display_name %>">
<td><button class="remove-unit ccx-button-link" aria-label="<%- interpolate(
gettext('Remove subsection %(subsectionDisplayName)s'), {subsectionDisplayName: child.display_name}, true) %>">
<i class="fa fa-remove" aria-hidden="true"></i> <%- gettext('remove') %>
</button></td>
</tr>
<% _.each(child.children, function(subchild) { %>
<tr class="vertical" data-dapth="3"
<tr class="vertical" data-depth="3"
data-location="<%= chapter.location %> <%= child.location %> <%= subchild.location %>">
<td class="unit">&nbsp;
<span class="sr"><%- gettext('Unit') %>&nbsp;</span>
<%= subchild.display_name %>
</td>
<td class="date start-date">
<button class="ccx-button-link">
<td class="date start-date no-link">
<% if (subchild.start) { %>
<%= subchild.start %>
<span class="sr"><%- gettext('Click to change') %></span>
</button>
<% } else { %>
<%
// Translators: Unit's aka vertical start date is set to Unscheduled when user has not set start date on corresponding subsection aka sequential.
%>
<%- gettext('Unscheduled') %>
<% } %>
</td>
<td class="date due-date">
<button class="ccx-button-link">
<td class="date due-date no-link">
<% if (subchild.due) { %>
<%= subchild.due %>
<span class="sr"><%- gettext('Click to change') %></span>
<% } else { %>
<%
// Translators: Unit's aka vertical due date is set to Unscheduled when user has not set due date on corresponding subsection aka sequential.
%>
<%- gettext('Unscheduled') %>
<% } %>
</td>
<td>
<button class="remove-unit ccx-button-link" aria-label="<%- interpolate(
gettext('Remove unit %(unitName)s'), {unitName: subchild.display_name}, true) %>">
<i class="fa fa-remove" aria-hidden="true"></i> <%- gettext('remove') %>
</button>
</td>
<td><button class="remove-unit ccx-button-link" aria-label="Remove unit <%= subchild.display_name %>">
<i class="fa fa-remove" aria-hidden="true"></i> <%- gettext('remove') %>
</button></td>
</tr>
<% }); %>
<% }); %>
<% }); %>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment