Merge pull request #832 from MITx/feature/cdodge/subsection-edit-page

Feature/cdodge/subsection edit page
This commit is contained in:
Calen Pennington
2012-10-09 06:08:15 -07:00
11 changed files with 990 additions and 42 deletions

View File

@@ -1,13 +1,14 @@
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
'''
cdodge: for a given Xmodule, return the course that it belongs to
NOTE: This makes a lot of assumptions about the format of the course location
Also we have to assert that this module maps to only one course item - it'll throw an
assert if not
'''
def get_course_location_for_item(location):
'''
cdodge: for a given Xmodule, return the course that it belongs to
NOTE: This makes a lot of assumptions about the format of the course location
Also we have to assert that this module maps to only one course item - it'll throw an
assert if not
'''
item_loc = Location(location)
# check to see if item is already a course, if so we can skip this
@@ -29,3 +30,18 @@ def get_course_location_for_item(location):
location = courses[0].location
return location
def get_lms_link_for_item(item):
if settings.LMS_BASE is not None:
lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format(
lms_base=settings.LMS_BASE,
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course
course_id = modulestore().get_containing_courses(item.location)[0].id,
location=item.location,
)
else:
lms_link = None
return lms_link

View File

@@ -43,7 +43,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import ADMIN_ROLE_NAME, EDITOR_ROLE_NAME
from .utils import get_course_location_for_item
from .utils import get_course_location_for_item, get_lms_link_for_item
from xmodule.templates import all_templates
@@ -143,13 +143,18 @@ def edit_subsection(request, location):
item = modulestore().get_item(location)
lms_link = get_lms_link_for_item(item)
# make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential':
return HttpResponseBadRequest
logging.debug('Start = {0}'.format(item.start))
return render_to_response('edit_subsection.html',
{'subsection': item,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link
})
@login_required
@@ -167,15 +172,7 @@ def edit_unit(request, location):
item = modulestore().get_item(location)
if settings.LMS_BASE is not None:
lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format(
lms_base=settings.LMS_BASE,
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course
course_id = modulestore().get_containing_courses(item.location)[0].id,
location=item.location,
)
else:
lms_link = None
lms_link = get_lms_link_for_item(item)
component_templates = defaultdict(list)
@@ -443,15 +440,29 @@ def save_item(request):
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# note, that the postback is not the complete metadata, as there's system metadata which is
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST['metadata']:
posted_metadata = request.POST['metadata']
# fetch original
existing_item = modulestore().get_item(item_location)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys():
# NOTE: We don't want clients to be able to delete 'system metadata' which are not intended to be user
# editable
if posted_metadata[metadata_key] is None and metadata_key not in existing_item.system_metadata_fields:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item.metadata:
del existing_item.metadata[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
existing_item.metadata.update(posted_metadata)
# commit to datastore
modulestore().update_metadata(item_location, existing_item.metadata)
return HttpResponse()

View File

@@ -31,6 +31,11 @@ $(document).ready(function() {
$('.sortable-unit-list').sortable();
$('.sortable-unit-list').disableSelection();
$('.sortable-unit-list').bind('sortstop', onUnitReordered);
// expand/collapse methods for optional date setters
$('.set-date').bind('click', showDateSetter);
$('.remove-date').bind('click', removeDateSetter);
});
// This method only changes the ordering of the child objects in a subsection
@@ -55,6 +60,27 @@ function onUnitReordered() {
});
}
function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
var input_date = $('#'+date_id).val();
var input_time = $('#'+time_id).val();
var edxTimeStr = null;
if (input_date != '') {
if (input_time == '')
input_time = '00:00';
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing
date = Date.parse(input_date+" "+input_time);
if (format == null)
format = 'yyyy-MM-ddTHH:mm';
edxTimeStr = date.toString(format);
}
return edxTimeStr;
}
function saveSubsection(e) {
e.preventDefault();
@@ -65,10 +91,18 @@ function saveSubsection(e) {
metadata = {};
for(var i=0; i< metadata_fields.length;i++) {
el = metadata_fields[i];
metadata[$(el).data("metadata-name")] = el.value;
el = metadata_fields[i];
metadata[$(el).data("metadata-name")] = el.value;
}
// Piece back together the date/time UI elements into one date/time string
// NOTE: our various "date/time" metadata elements don't always utilize the same formatting string
// so make sure we're passing back the correct format
metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time');
metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm');
// reordering is done through immediate callbacks when the resorting has completed in the UI
children =[];
$.ajax({
@@ -198,8 +232,22 @@ function hideHistoryModal(e) {
$modalCover.hide();
}
function showDateSetter(e) {
e.preventDefault();
var $block = $(this).closest('.due-date-input');
$(this).hide();
$block.find('.date-setter').show();
}
function removeDateSetter(e) {
e.preventDefault();
var $block = $(this).closest('.due-date-input');
$block.find('.date-setter').hide();
$block.find('.set-date').show();
// clear out the values
$block.find('.date').val('');
$block.find('.time').val('');
}

View File

@@ -1,9 +1,18 @@
<%inherit file="base.html" />
<%!
from time import mktime
import dateutil.parser
import logging
from datetime import datetime
%>
<%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">subsection</%block>
<%block name="title">CMS Subsection</%block>
<%namespace name="units" file="widgets/units.html" />
<%namespace name='static' file='static_content.html'/>
<%namespace name='datetime' module='datetime'/>
<%block name="content">
<div class="main-wrapper">
@@ -15,14 +24,14 @@
<input type="text" value="${subsection.metadata['display_name']}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div>
<label>Subtitle:</label>
<input type="text" value="${subsection.metadata['subtitle'] if 'subtitle' in subsection.metadata else ''}" class="unit-subtitle" data-metadata-name="subtitle"/>
<label>Format:</label>
<input type="text" value="${subsection.metadata['format'] if 'format' in subsection.metadata else ''}" class="unit-subtitle" data-metadata-name="subtitle"/>
</div>
<div class="unit-list">
<label>Units:</label>
${units.enum_units(subsection)}
</div>
<div>
<div class='wip-box'>
<label>Policy:</label>
<textarea class="text-editor">Policy blah, blah, blah…</textarea>
</div>
@@ -35,31 +44,52 @@
<div class="window-contents">
<div class="scheduled-date-input row">
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
<div class="date-setter">
<input type="text" value="10/22/2012" class="date-input" />
<input type="text" value="6:00 am" class="time-input" />
<div class="datepair" data-language="javascript">
<%
start_time = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
%>
<input type="text" id="start_date" value="${start_time.strftime('%m/%d/%Y') if start_time is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15'/>
<input type="text" id="start_time" value="${start_time.strftime('%H:%M') if start_time is not None else ''}" placeholder="HH:MM" class="time" size='10'/>
</div>
<p class="notice">The date above differs from the release date of Week 1 10/10/2012 at 12:00 am. <a href="#" class="sync-date">Sync to Week 1.</a></p>
<p class="notice wip-box">The date above differs from the release date of Week 1 10/10/2012 at 12:00 am. <a href="#" class="sync-date">Sync to Week 1.</a></p>
</div>
<div class="due-date-input row">
<label>Due date:</label>
<a href="#" class="set-date">Set a due date</a>
<div class="date-setter">
<p class="date-description"><input type="text" value="10/20/2012" class="date-input" /> <input type="text" value="6:00 am" class="time-input" />
<div class="datepair date-setter">
<p class="date-description">
<%
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
%>
<input type="text" id="due_date" value="${due_date.strftime('%Y-%m-%d') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' />
<input type="text" id="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' />
<a href="#" class="remove-date">Remove due date</a>
</p>
</div>
</div>
<div class="visibility row">
<label>Visibility:<!-- <span class="description">Shows or hides this subsection and the units within it.</span>--></label>
<a href="#" class="toggle-off">hide</a><a href="#" class="large-toggle"></a><a href="#" class="toggle-on">show</a>
</div>
<div class="row unit-actions">
<a href="#" class="save-button save-subsection" data-id="${subsection.location}">Save</a>
<a href="preview.html" target="_blank" class="preview-button">Preview</a>
<a href="${lms_link}" target="_blank" class="preview-button">Preview</a>
</div>
</div>
</div>
</div>
</div>
</%block>
<%block name="jsextra">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript">
$(document).ready(function() {
if ($('#due_date').val() != '') {
var $block = $('.set-date').closest('.due-date-input');
$('.set-date').hide();
$block.find('.date-setter').show();
}
})
</script>
</%block>

View File

@@ -17,7 +17,7 @@ This def will enumerate through a passed in subsection and list all of the units
<a href="${reverse('edit_unit', args=[unit.location])}" class="private-item">
<span class="${unit.category}-icon"></span>
${unit.display_name}
<span class="private-tag">- private</span>
<span class="private-tag wip">- private</span>
</a>
% if actions:
<div class="item-actions">