Merge pull request #832 from MITx/feature/cdodge/subsection-edit-page
Feature/cdodge/subsection edit page
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user