Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into feature/cdodge/cas-crud-section-subsection

Conflicts:
	cms/static/js/base.js
This commit is contained in:
Chris Dodge
2012-10-10 13:50:22 -04:00
39 changed files with 1096 additions and 182 deletions

View File

@@ -26,4 +26,4 @@ class Command(BaseCommand):
print "Importing. Data_dir={data}, course_dirs={courses}".format(
data=data_dir,
courses=course_dirs)
import_from_xml(modulestore(), data_dir, course_dirs, load_error_modules=False)
import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False)

View File

@@ -141,8 +141,6 @@ class AuthTestCase(ContentStoreTestCase):
"""Make sure pages that do require login work."""
auth_pages = (
reverse('index'),
reverse('edit_item'),
reverse('save_item'),
)
# These are pages that should just load when the user is logged in
@@ -181,6 +179,7 @@ class AuthTestCase(ContentStoreTestCase):
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class EditTestCase(ContentStoreTestCase):
@@ -195,17 +194,17 @@ class EditTestCase(ContentStoreTestCase):
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
def check_edit_item(self, test_course_name):
def check_edit_unit(self, test_course_name):
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
for descriptor in modulestore().get_items(Location(None, None, None, None, None)):
for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)):
print "Checking ", descriptor.location.url()
print descriptor.__class__, descriptor.location
resp = self.client.get(reverse('edit_item'), {'id': descriptor.location.url()})
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
def test_edit_item_toy(self):
self.check_edit_item('toy')
def test_edit_unit_toy(self):
self.check_edit_unit('toy')
def test_edit_item_full(self):
self.check_edit_item('full')
def test_edit_unit_full(self):
self.check_edit_unit('full')

View File

@@ -1,6 +1,9 @@
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.draft import DRAFT
from xmodule.modulestore.exceptions import ItemNotFoundError
def get_course_location_for_item(location):
'''
@@ -32,16 +35,42 @@ def get_course_location_for_item(location):
return location
def get_lms_link_for_item(item):
def get_lms_link_for_item(location):
location = Location(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,
course_id = modulestore().get_containing_courses(location)[0].id,
location=location,
)
else:
lms_link = None
return lms_link
class UnitState(object):
draft = 'draft'
private = 'private'
public = 'public'
def compute_unit_state(unit):
"""
Returns whether this unit is 'draft', 'public', or 'private'.
'draft' content is in the process of being edited, but still has a previous
version visible in the LMS
'public' content is locked and visible in the LMS
'private' content is editabled and not visible in the LMS
"""
if unit.metadata.get('is_draft', False):
try:
modulestore('direct').get_item(unit.location)
return UnitState.draft
except ItemNotFoundError:
return UnitState.private
else:
return UnitState.public

View File

@@ -1,11 +1,12 @@
from util.json_request import expect_json
import json
import os
import logging
import sys
import mimetypes
import StringIO
import exceptions
import json
import logging
import mimetypes
import os
import StringIO
import sys
import time
from collections import defaultdict
from uuid import uuid4
@@ -43,7 +44,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 INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME
from .utils import get_course_location_for_item, get_lms_link_for_item
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state
from xmodule.templates import all_templates
@@ -157,20 +158,37 @@ def edit_subsection(request, location):
item = modulestore().get_item(location)
lms_link = get_lms_link_for_item(item)
lms_link = get_lms_link_for_item(location)
# make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential':
return HttpResponseBadRequest
logging.debug('Start = {0}'.format(item.start))
parent_locs = modulestore().get_parent_locations(location)
# we're for now assuming a single parent
if len(parent_locs) != 1:
logging.error('Multiple (or none) parents have been found for {0}'.format(location))
# this should blow up if we don't find any parents, which would be erroneous
parent = modulestore().get_item(parent_locs[0])
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict((key,value) for key, value in item.metadata.iteritems()
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
logging.debug(policy_metadata)
return render_to_response('edit_subsection.html',
{'subsection': item,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link
'lms_link': lms_link,
'parent_item' : parent,
'policy_metadata' : policy_metadata
})
@login_required
def edit_unit(request, location):
"""
@@ -186,7 +204,8 @@ def edit_unit(request, location):
item = modulestore().get_item(location)
lms_link = get_lms_link_for_item(item)
# The non-draft location
lms_link = get_lms_link_for_item(item.location._replace(revision=None))
component_templates = defaultdict(list)
@@ -213,14 +232,25 @@ def edit_unit(request, location):
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location)
containing_section = modulestore().get_item(containing_section_locs[0])
unit_state = compute_unit_state(item)
try:
published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date'))
except TypeError:
published_date = None
return render_to_response('unit.html', {
'unit': item,
'unit_location': location,
'components': components,
'component_templates': component_templates,
'lms_link': lms_link,
'draft_preview_link': lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
'published_date': published_date,
})
@@ -238,7 +268,6 @@ def preview_component(request, location):
})
def user_author_string(user):
'''Get an author string for commits by this user. Format:
first last <email@email.com>.
@@ -407,6 +436,13 @@ def get_module_previews(request, descriptor):
preview_html.append(module.get_html())
return preview_html
def _xmodule_recurse(item, action):
for child in item.get_children():
_xmodule_recurse(child, action)
action(item)
def _delete_item(item, recurse=False):
if recurse:
children = item.get_children()
@@ -430,8 +466,11 @@ def delete_item(request):
item = modulestore().get_item(item_location)
_delete_item(item, delete_children)
if delete_children:
_xmodule_recurse(item, lambda i: modulestore().delete_item(i.location))
else:
modulestore().delete_item(item.location)
return HttpResponse()
@@ -465,9 +504,12 @@ def save_item(request):
# 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:
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in existing_item.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# 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]
@@ -482,6 +524,51 @@ def save_item(request):
return HttpResponse()
@login_required
@expect_json
def create_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
# This clones the existing item location to a draft location (the draft is implicit,
# because modulestore is a Draft modulestore)
modulestore().clone_item(location, location)
return HttpResponse()
@login_required
@expect_json
def publish_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
return HttpResponse()
@login_required
@expect_json
def unpublish_unit(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
item = modulestore().get_item(location)
_xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
return HttpResponse()
@login_required
@expect_json
def clone_item(request):
@@ -506,7 +593,12 @@ def clone_item(request):
new_item.metadata['display_name'] = display_name
modulestore().update_metadata(new_item.location.url(), new_item.own_metadata)
modulestore().update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
if parent_location.category not in ('vertical',):
parent_update_modulestore = modulestore('direct')
else:
parent_update_modulestore = modulestore()
parent_update_modulestore.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
@@ -688,3 +780,9 @@ def asset_index(request, location):
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
def static_pages(request, org, course, coursename):
return render_to_response('static-pages.html', {})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})

View File

@@ -14,17 +14,23 @@ LOGGING = get_logger_config(ENV_ROOT / "log",
tracking_filename="tracking.log",
debug=True)
modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
'OPTIONS': modulestore_options
}
}

View File

@@ -38,17 +38,23 @@ STATICFILES_DIRS += [
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
]
modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
'OPTIONS': modulestore_options
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
}
}

View File

@@ -35,9 +35,9 @@ class CMS.Views.ModuleEdit extends Backbone.View
return _metadata
cloneTemplate: (template) ->
cloneTemplate: (parent, template) ->
$.post("/clone_item", {
parent_location: @$el.parent().data('id')
parent_location: parent
template: template
}, (data) =>
@model.set(id: data.id)

View File

@@ -5,9 +5,35 @@ class CMS.Views.UnitEdit extends Backbone.View
'click .new-component-templates .new-component-template a': 'saveNewComponent'
'click .new-component-templates .cancel-button': 'closeNewComponent'
'click .new-component-button': 'showNewComponentForm'
'click .unit-actions .save-button': 'save'
'click #save-draft': 'saveDraft'
'click #delete-draft': 'deleteDraft'
'click #create-draft': 'createDraft'
'click #publish-draft': 'publishDraft'
'change #visibility': 'setVisibility'
initialize: =>
@visibilityView = new CMS.Views.UnitEdit.Visibility(
el: @$('#visibility')
model: @model
)
@saveView = new CMS.Views.UnitEdit.SaveDraftButton(
el: @$('#save-draft')
model: @model
)
@locationView = new CMS.Views.UnitEdit.LocationState(
el: @$('.section-item.editing a')
model: @model
)
@nameView = new CMS.Views.UnitEdit.NameEdit(
el: @$('.unit-name-input')
model: @model
)
@model.on('change:state', @render)
@$newComponentItem = @$('.new-component-item')
@$newComponentTypePicker = @$('.new-component')
@$newComponentTemplatePickers = @$('.new-component-templates')
@@ -15,7 +41,13 @@ class CMS.Views.UnitEdit extends Backbone.View
@$('.components').sortable(
handle: '.drag-handle'
update: (event, ui) => @saveOrder()
update: (event, ui) => @model.set('children', @components())
helper: 'clone'
opacity: '0.5'
placeholder: 'component-placeholder'
forcePlaceholderSize: true
axis: 'y'
items: '> .component'
)
@$('.component').each((idx, element) =>
@@ -26,10 +58,10 @@ class CMS.Views.UnitEdit extends Backbone.View
id: $(element).data('id'),
)
)
update: (event, ui) => @model.set('children', @components())
)
@model.components = @components()
# New component creation
showNewComponentForm: (event) =>
event.preventDefault()
@$newComponentItem.addClass('adding')
@@ -56,21 +88,31 @@ class CMS.Views.UnitEdit extends Backbone.View
event.preventDefault()
editor = new CMS.Views.ModuleEdit(
onDelete: @deleteComponent
model: new CMS.Models.Module()
)
@$newComponentItem.before(editor.$el)
editor.cloneTemplate($(event.currentTarget).data('location'))
editor.cloneTemplate(
@$el.data('id'),
$(event.currentTarget).data('location')
)
@closeNewComponent(event)
components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
saveOrder: =>
@model.save(
children: @components()
)
wait: (value) =>
@$('.unit-body').toggleClass("waiting", value)
render: =>
if @model.hasChanged('state')
@$el.toggleClass("edit-state-#{@model.previous('state')} edit-state-#{@model.get('state')}")
@wait(false)
saveDraft: =>
@model.save()
deleteComponent: (event) =>
$component = $(event.currentTarget).parents('.component')
@@ -78,6 +120,94 @@ class CMS.Views.UnitEdit extends Backbone.View
id: $component.data('id')
}, =>
$component.remove()
@saveOrder()
@model.set('children', @components())
)
deleteDraft: (event) ->
@wait(true)
$.post('/delete_item', {
id: @$el.data('id')
delete_children: true
}, =>
window.location.reload()
)
createDraft: (event) ->
@wait(true)
$.post('/create_draft', {
id: @$el.data('id')
}, =>
@model.set('state', 'draft')
)
publishDraft: (event) ->
@wait(true)
@saveDraft()
$.post('/publish_draft', {
id: @$el.data('id')
}, =>
@model.set('state', 'public')
)
setVisibility: (event) ->
if @$('#visibility').val() == 'private'
target_url = '/unpublish_unit'
else
target_url = '/publish_draft'
@wait(true)
$.post(target_url, {
id: @$el.data('id')
}, =>
@model.set('state', @$('#visibility').val())
)
class CMS.Views.UnitEdit.NameEdit extends Backbone.View
events:
"keyup .unit-display-name-input": "saveName"
initialize: =>
@model.on('change:metadata', @render)
@saveName
render: =>
@$('.unit-display-name-input').val(@model.get('metadata').display_name)
saveName: =>
# Treat the metadata dictionary as immutable
metadata = $.extend({}, @model.get('metadata'))
metadata.display_name = @$('.unit-display-name-input').val()
@model.set('metadata', metadata)
class CMS.Views.UnitEdit.LocationState extends Backbone.View
initialize: =>
@model.on('change:state', @render)
render: =>
@$el.toggleClass("#{@model.previous('state')}-item #{@model.get('state')}-item")
class CMS.Views.UnitEdit.Visibility extends Backbone.View
initialize: =>
@model.on('change:state', @render)
@render()
render: =>
@$el.val(@model.get('state'))
class CMS.Views.UnitEdit.SaveDraftButton extends Backbone.View
initialize: =>
@model.on('change:children', @enable)
@model.on('change:metadata', @enable)
@model.on('sync', @disable)
@disable()
disable: =>
@$el.addClass('disabled')
enable: =>
@$el.removeClass('disabled')

View File

@@ -36,6 +36,7 @@ $(document).ready(function() {
$('.set-date').bind('click', showDateSetter);
$('.remove-date').bind('click', removeDateSetter);
<<<<<<< HEAD
// add new/delete section
$('.new-courseware-section-button').bind('click', addNewSection);
$('.delete-section-button').bind('click', deleteSection);
@@ -43,9 +44,42 @@ $(document).ready(function() {
// add new/delete subsection
$('.new-subsection-item').bind('click', addNewSubsection);
$('.delete-subsection-button').bind('click', deleteSubsection);
=======
// add/remove policy metadata button click handlers
$('.add-policy-data').bind('click', addPolicyMetadata);
$('.remove-policy-data').bind('click', removePolicyMetadata);
$('.sync-date').bind('click', syncReleaseDate);
>>>>>>> 2cf14ae339aed0150be353cbcac25c377956b7ab
});
function syncReleaseDate(e) {
e.preventDefault();
$("#start_date").val("");
$("#start_time").val("");
}
function addPolicyMetadata(e) {
e.preventDefault();
var template =$('#add-new-policy-element-template > li');
var newNode = template.clone();
var _parent_el = $(this).parent('ol:.policy-list');
newNode.insertBefore('.add-policy-data');
$('.remove-policy-data').bind('click', removePolicyMetadata);
}
function removePolicyMetadata(e) {
e.preventDefault();
policy_name = $(this).data('policy-name');
var _parent_el = $(this).parent('li:.policy-list-element');
if ($(_parent_el).hasClass("new-policy-list-element"))
_parent_el.remove();
else
_parent_el.appendTo("#policy-to-delete");
}
// This method only changes the ordering of the child objects in a subsection
function onUnitReordered() {
var subsection_id = $(this).data('subsection-id');
@@ -94,7 +128,7 @@ function saveSubsection(e) {
var id = $(this).data('id');
// pull all metadata editable fields on page
// pull all 'normalized' metadata editable fields on page
var metadata_fields = $('input[data-metadata-name]');
metadata = {};
@@ -103,6 +137,20 @@ function saveSubsection(e) {
metadata[$(el).data("metadata-name")] = el.value;
}
// now add 'free-formed' metadata which are presented to the user as dual input fields (name/value)
$('ol.policy-list > li.policy-list-element').each( function(i, element) {
name = $(element).children('.policy-list-name').val();
val = $(element).children('.policy-list-value').val();
metadata[name] = val;
});
// now add any 'removed' policy metadata which is stored in a separate hidden div
// 'null' presented to the server means 'remove'
$("#policy-to-delete > li.policy-list-element").each(function(i, element) {
name = $(element).children('.policy-list-name').val();
if (name != "")
metadata[name] = null;
});
// 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

View File

@@ -29,6 +29,10 @@ h1 {
margin: 36px 6px;
}
.waiting {
opacity: 0.1;
}
.page-actions {
float: right;
margin-top: 42px;
@@ -128,13 +132,15 @@ label {
}
.new-unit-item,
.new-subsection-item {
.new-subsection-item,
.new-policy-item {
@include grey-button;
margin: 5px 8px;
padding: 3px 10px 4px 10px;
font-size: 10px;
.new-folder-icon,
.new-policy-icon,
.new-unit-icon {
position: relative;
top: 2px;

View File

@@ -16,6 +16,18 @@
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0));
@include transition(background-color .15s, box-shadow .15s);
&.disabled {
border: 1px solid $lightGrey !important;
border-radius: 3px !important;
background: $lightGrey !important;
color: $darkGrey !important;
pointer-events: none;
cursor: none;
&:hover {
box-shadow: 0 0 0 0 !important;
}
}
&:hover {
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15));
}
@@ -161,13 +173,33 @@
background: #fffcf1;
}
.draft-item,
.hidden-item,
.draft-item:after,
.public-item:after,
.private-item:after {
margin-left: 3px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
}
.draft-item:after {
content: "- draft";
}
.public-item:after {
content: "- public";
}
.private-item:after {
content: "- private";
}
.public-item,
.private-item {
color: #a4aab7;
}
.has-new-draft-item {
.draft-item {
color: #9f7d10;
}
}

View File

@@ -117,9 +117,8 @@
}
.draft-tag,
.hidden-tag,
.private-tag,
.has-new-draft-tag {
.public-tag,
.private-tag {
margin-left: 3px;
font-size: 9px;
font-weight: 600;
@@ -127,7 +126,7 @@
color: #a4aab7;
}
.has-new-draft-tag {
.draft-tag {
color: #9f7d10;
}
@@ -171,6 +170,14 @@
background: url(../img/new-unit-icon.png) right no-repeat;
}
.new-policy-icon {
display: inline-block;
width: 23px;
height: 12px;
margin-right: 8px;
background: url(../img/new-unit-icon.png) right no-repeat;
}
.textbook-icon {
display: inline-block;
width: 32px;
@@ -244,4 +251,4 @@
margin-left: 10px;
vertical-align: middle;
background: url(../img/blue-spinner.gif) no-repeat;
}
}

View File

@@ -13,6 +13,10 @@ body.no-header {
color: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset);
.left {
width: 700px;
}
.drop-icon {
margin-left: 5px;
font-size: 11px;

View File

@@ -0,0 +1,64 @@
.static-pages {
.new-static-page-button {
@include grey-button;
display: block;
text-align: center;
padding: 12px 0;
}
.static-page-item {
position: relative;
margin: 10px 0;
padding: 22px 20px;
border: 1px solid $darkGrey;
border-radius: 3px;
background: #fff;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
.page-name {
font-size: 19px;
font-weight: 700;
}
.item-actions {
margin-top: 19px;
margin-right: 12px;
}
}
}
.edit-static-page {
.main-wrapper {
margin-top: 40px;
}
.static-page-details {
@extend .window;
padding: 32px 40px;
.row {
border: none;
}
}
.page-display-name-input {
width: 100%;
font-size: 20px;
}
.page-contents {
@include box-sizing(border-box);
width: 100%;
height: 360px;
padding: 15px;
border: 1px solid #b0b6c2;
border-radius: 2px;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3));
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
font-family: Monaco, monospace;
font-size: 13px;
color: #3c3c3c;
outline: 0;
}
}

View File

@@ -75,19 +75,17 @@
&.editing {
border-color: #6696d7;
&:hover {
.drag-handle,
.component-actions a {
background-color: $blue;
}
.drag-handle {
border-color: $blue;
}
.drag-handle,
.component-actions {
display: none;
}
}
.rendered-component {
&.component-placeholder {
border-color: #6696d7;
}
.xmodule_display {
padding: 40px 20px 20px;
}
@@ -230,14 +228,24 @@
@include edit-box;
display: none;
padding: 20px;
border-radius: 0 0 3px 3px;
border-radius: 2px 2px 0 0;
@include linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1));
background-color: $blue;
color: #fff;
@include box-shadow(none);
.metadata_edit {
margin-bottom: 20px;
font-size: 13px;
li {
margin-bottom: 10px;
}
label {
display: inline-block;
margin-right: 10px;
}
}
.CodeMirror {
@@ -245,6 +253,7 @@
}
h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: 700;
}
@@ -394,3 +403,37 @@
}
}
}
.edit-state-draft {
.visibility {
display: none;
}
#create-draft {
display: none;
}
}
.edit-state-public {
#save-draft,
#delete-draft,
#publish-draft,
.component-actions,
.new-component-item,
#published-alert {
display: none;
}
.drag-handle {
display: none !important;
}
}
.edit-state-private {
#delete-draft,
#publish-draft,
#published-alert,
#create-draft, {
display: none;
}
}

View File

@@ -14,6 +14,7 @@
@import "subsection";
@import "unit";
@import "assets";
@import "static-pages";
@import "course-info";
@import "landing";
@import "graphics";

View File

@@ -1,9 +1,3 @@
${preview}
<div class="component-actions">
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
</div>
<a href="#" class="drag-handle"></a>
<div class="component-editor">
<div class="module-editor">
${editor}
@@ -11,4 +5,9 @@ ${preview}
<a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</a>
</div>
<div class="component-actions">
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
</div>
<a href="#" class="drag-handle"></a>
${preview}

View File

@@ -0,0 +1,41 @@
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Edit Static Page</%block>
<%block name="bodyclass">edit-static-page</%block>
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<div class="main-column">
<article class="static-page-details">
<div class="row">
<label>Display Name:</label>
<input type="text" value="Syllabus" class="page-display-name-input" data-metadata-name="display_name"/>
</div>
<div class="row">
<label>Page Content:</label>
<textarea class="page-contents"></textarea>
</div>
</article>
</div>
<div class="sidebar">
<div class="unit-properties window">
<h4>Page Settings</h4>
<div class="window-contents">
<div class="row visibility">
<label class="inline-label">Visibility:</label>
<select>
<option>Public</option>
<option>Private</option>
</select>
</div>
<div class="row unit-actions">
<a href="#" class="save-button">Save</a>
<a href="#" target="_blank" class="preview-button">Preview</a>
</div>
</div>
</div>
</div>
</div>
</div>
</%block>

View File

@@ -19,25 +19,42 @@
<div class="inner-wrapper">
<div class="main-column">
<article class="subsection-body window">
<div class="subsection-name-input">
<label>Display Name:</label>
<input type="text" value="${subsection.metadata['display_name']}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div>
<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 class='wip-box'>
<label>Policy:</label>
<textarea class="text-editor">Policy blah, blah, blah…</textarea>
</div>
<div class="subsection-name-input">
<label>Display Name:</label>
<input type="text" value="${subsection.metadata['display_name']}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div>
<label>Format:</label>
<input type="text" value="${subsection.metadata['format'] if 'format' in subsection.metadata else ''}" class="unit-subtitle" data-metadata-name="format"/>
</div>
<div class="unit-list">
<label>Units:</label>
${units.enum_units(subsection)}
</div>
<div>
<label>Policy:</label>
<ol class='policy-list'>
% for policy_name in policy_metadata.keys():
<li class="policy-list-element">
<input type="text" class="policy-list-name" name="${policy_name}" value="${policy_name}" disabled size="15"/>:&nbsp;<input type="text" class="policy-list-value" name="${policy_metadata[policy_name]}" value="${policy_metadata[policy_name]}" size="40"/><a href="#" class="delete-icon remove-policy-data"></a>
</li>
% endfor
<a href="#" class="new-policy-item add-policy-data" >
<span class="new-policy-icon"></span>New Policy Data
</a>
</ol>
</div>
</article>
</div>
<div id="policy-to-delete" style="display:none">
</div>
<div id="add-new-policy-element-template" style="display:none">
<li class="policy-list-element new-policy-list-element"><input type="text" class="policy-list-name" autocomplete="off" size="15"/>:&nbsp;<input type="text" class="policy-list-value" size=40 autocomplete="off"/><a href="#" class="delete-icon remove-policy-data"></a></li>
</div>
<div class="sidebar">
<div class="unit-properties window">
<h4>Subsection Settings</h4>
@@ -46,12 +63,15 @@
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
<div class="datepair" data-language="javascript">
<%
start_time = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.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'/>
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
<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>
% if subsection.start != parent_item.start and subsection.start:
<p class="notice">The date above differs from the release date of ${parent_item.display_name} ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}. <a href="#" class="sync-date">Sync to ${parent_item.display_name}.</a></p>
% endif
</div>
<div class="due-date-input row">
<label>Due date:</label>
@@ -62,8 +82,8 @@
# 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' />
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<a href="#" class="remove-date">Remove due date</a>
</p>
</div>
@@ -85,6 +105,7 @@
<script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript">
$(document).ready(function() {
// expand the due-date area if the values are set
if ($('#due_date').val() != '') {
var $block = $('.set-date').closest('.due-date-input');
$('.set-date').hide();

View File

@@ -0,0 +1,41 @@
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Static Pages</%block>
<%block name="bodyclass">static-pages</%block>
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Static Pages</h1>
<div class="page-actions">
</div>
<article class="static-page-overview">
<a href="#" class="new-static-page-button wip-box"><span class="plus-icon"></span> New Static Page</a>
<ul class="static-page-list">
<li class="static-page-item">
<a href="#" class="page-name">Course Info</a>
<div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a>
</div>
</li>
<li class="static-page-item">
<a href="#" class="page-name">Textbook</a>
<div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a>
</div>
</li>
<li class="static-page-item">
<a href="#" class="page-name">Syllabus</a>
<div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a>
</div>
</li>
</ul>
</article>
</div>
</div>
</%block>

View File

@@ -8,18 +8,27 @@
new CMS.Views.UnitEdit({
el: $('.main-wrapper'),
model: new CMS.Models.Module({
id: '${unit.location.url()}'
id: '${unit_location}',
state: '${unit_state}'
})
});
</script>
</%block>
<%block name="content">
<div class="main-wrapper">
<div class="main-wrapper edit-state-${unit_state}" data-id="${unit_location}">
<div class="inner-wrapper">
<div class="alert" id="published-alert">
<p class="alert-message"><strong>You are editing a draft.</strong>
% if published_date:
This unit was originally published on ${published_date}.
% endif
</p>
<a href="${published_preview_link}" target="_blank" class="alert-action secondary">Preview the published version</a>
</div>
<div class="main-column">
<article class="unit-body window">
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name}" class="unit-display-name-input" /></p>
<ol class="components" data-id="${unit.location.url()}">
<ol class="components">
% for id in components:
<li class="component" data-id="${id}"/>
% endfor
@@ -60,31 +69,26 @@
</article>
</div>
<div class="sidebar wip-box">
<div class="sidebar">
<div class="unit-properties window">
<h4>Unit Properties</h4>
<div class="window-contents">
<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" />
<a href="#" class="remove-date">Remove due date</a>
</div>
</div>
<div class="row visibility">
<label class="inline-label">Visibility:</label>
<select>
<option>Public</option>
<option>Private</option>
<select id='visibility'>
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</div>
<a id="create-draft" href="#">This unit has been published. Click here to edit it.</a>
<a id="publish-draft" href="#">This unit has already been published. Click here to release your changes to it</a>
<div class="row status">
<p>This unit is scheduled to be released to <strong>students</strong> on <strong>10/12/2012</strong> with the subsection <a href="#">"Administrivia and Circuit Elements."</a></p>
<p>This unit is scheduled to be released to <strong>students</strong> on <strong>${subsection.start}</strong> with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p>
</div>
<div class="row unit-actions">
<a href="#" class="save-button">Save</a>
<a href="${lms_link}" target="_blank" class="preview-button">Preview</a>
<a id="save-draft" href="#" class="save-button">Save Draft</a>
<a id="delete-draft" href="#" class="save-button">Delete Draft</a>
<a href="${draft_preview_link}" target="_blank" class="preview-button">Preview</a>
</div>
</div>
</div>
@@ -114,4 +118,4 @@
</div>
</div>
</%block>
</%block>

View File

@@ -3,12 +3,13 @@
<header class="primary-header">
<nav class="inner-wrapper">
<div class="left">
<a href="/"><span class="home-icon"></span></a>
<a href="/"><span class="home-icon"></span></a>
<a href="#" class="class-name wip-box">6.002x Circuits and Electronics <span class="drop-icon"></span></a>
<ul class="class-nav">
<li><a href="overview-full.html" class="active">Courseware</a></li>
<li><a href="updates.html" class="wip-box">Course Info</a></li>
<li><a href="textbook.html" class="wip-box">Textbook</a></li>
<li><a href="#" class="wip-box">Pages</a></li>
<li><a href="#" class="wip-box">Assets</a></li>
<li><a href="#" class="wip-box">Users</a></li>
</ul>
</div>
<div class="right">
@@ -18,7 +19,6 @@
% else:
<a href="${reverse('login')}">Log in</a>
% endif
</ul>
</nav>
</header>

View File

@@ -3,7 +3,7 @@
<h3>Metadata</h3>
<ul>
% for keyname in editable_metadata_fields:
<li>${keyname}: <input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' /></li>
<li><label>${keyname}:</label> <input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' /></li>
% endfor
</ul>
</section>

View File

@@ -1,4 +1,5 @@
<%! from django.core.urlresolvers import reverse %>
<%! from contentstore.utils import compute_unit_state %>
<!--
This def will enumerate through a passed in subsection and list all of the units
@@ -8,16 +9,16 @@ This def will enumerate through a passed in subsection and list all of the units
% for unit in subsection.get_children():
<li class="leaf unit" data-id="${unit.location}">
<%
unit_state = compute_unit_state(unit)
if unit.location == selected:
selected_class = 'editing'
else:
selected_class = ''
%>
<div class="section-item ${selected_class}">
<a href="${reverse('edit_unit', args=[unit.location])}" class="private-item">
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
<span class="${unit.category}-icon"></span>
${unit.display_name}
<span class="private-tag wip">- private</span>
</a>
% if actions:
<div class="item-actions">

View File

@@ -1,8 +1,6 @@
from django.conf import settings
from django.conf.urls import patterns, include, url
import django.contrib.auth.views
# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()
@@ -15,12 +13,15 @@ urlpatterns = ('',
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^delete_item$', 'contentstore.views.delete_item', name='delete_item'),
url(r'^clone_item$', 'contentstore.views.clone_item', name='clone_item'),
url(r'^create_draft$', 'contentstore.views.create_draft', name='create_draft'),
url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_draft'),
url(r'^unpublish_unit$', 'contentstore.views.unpublish_unit', name='unpublish_unit'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
'contentstore.views.course_index', name='course_index'),
url(r'^github_service_hook$', 'github_sync.views.github_post_receive'),
url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'contentstore.views.preview_dispatch', name='preview_dispatch'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$',
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$',
'contentstore.views.upload_asset', name='upload_asset'),
url(r'^manage_users/(?P<location>.*?)$', 'contentstore.views.manage_users', name='manage_users'),
url(r'^add_user/(?P<location>.*?)$',
@@ -30,6 +31,8 @@ urlpatterns = ('',
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user'),
url(r'^assets/(?P<location>.*?)$', 'contentstore.views.asset_index', name='asset_index'),
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages', name='static_pages'),
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
# temporary landing page for a course
url(r'^landing/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing')
@@ -54,6 +57,6 @@ urlpatterns += (
if settings.DEBUG:
## Jasmine
urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
urlpatterns = patterns(*urlpatterns)