diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 1d15f1e7df..a8ec2c2685 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -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) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 429774c91e..b5dfc3c4fe 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -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') diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 4000f011ba..bc33c7d1f6 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -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 diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index e2ecfd8ba9..a5aab97f05 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -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 . @@ -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', {}) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 8401fa5c15..e5548df2d4 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -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 } } diff --git a/cms/envs/test.py b/cms/envs/test.py index 7dcd32caab..217ed0e573 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -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 } } diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee index 2326756dc8..85c099ab9a 100644 --- a/cms/static/coffee/src/views/module_edit.coffee +++ b/cms/static/coffee/src/views/module_edit.coffee @@ -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) diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee index 1180b17471..53eb11d511 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -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') \ No newline at end of file diff --git a/cms/static/js/base.js b/cms/static/js/base.js index d9585fcf66..d81f3f9f4d 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -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 diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index fe97c9b975..ac93a0f60f 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -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; diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss index c1d8245e36..efe4556b50 100644 --- a/cms/static/sass/_cms_mixins.scss +++ b/cms/static/sass/_cms_mixins.scss @@ -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; } } diff --git a/cms/static/sass/_graphics.scss b/cms/static/sass/_graphics.scss index 65c827981a..83f8816b47 100644 --- a/cms/static/sass/_graphics.scss +++ b/cms/static/sass/_graphics.scss @@ -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; -} \ No newline at end of file +} diff --git a/cms/static/sass/_header.scss b/cms/static/sass/_header.scss index 84e3ba1588..d70f53b4df 100644 --- a/cms/static/sass/_header.scss +++ b/cms/static/sass/_header.scss @@ -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; diff --git a/cms/static/sass/_static-pages.scss b/cms/static/sass/_static-pages.scss new file mode 100644 index 0000000000..628d537f90 --- /dev/null +++ b/cms/static/sass/_static-pages.scss @@ -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; + } +} \ No newline at end of file diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 7ed7e3ee7e..5acc7f54b0 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -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; + } +} diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 8743790bba..628332d9ee 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -14,6 +14,7 @@ @import "subsection"; @import "unit"; @import "assets"; +@import "static-pages"; @import "course-info"; @import "landing"; @import "graphics"; diff --git a/cms/templates/component.html b/cms/templates/component.html index f0d266e2e1..648a5e3d98 100644 --- a/cms/templates/component.html +++ b/cms/templates/component.html @@ -1,9 +1,3 @@ -${preview} -
- Edit - Delete -
-
${editor} @@ -11,4 +5,9 @@ ${preview} Save Cancel
- +
+ Edit + Delete +
+ +${preview} \ No newline at end of file diff --git a/cms/templates/edit-static-page.html b/cms/templates/edit-static-page.html new file mode 100644 index 0000000000..f86d00989e --- /dev/null +++ b/cms/templates/edit-static-page.html @@ -0,0 +1,41 @@ +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="title">Edit Static Page +<%block name="bodyclass">edit-static-page + +<%block name="content"> +
+
+
+
+
+ + +
+
+ + +
+
+
+ +
+
+ \ No newline at end of file diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index 3fa1e135dd..e6eeed0963 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -19,25 +19,42 @@
-
- - -
-
- - -
-
- - ${units.enum_units(subsection)} -
-
- - -
+
+ + +
+
+ + +
+
+ + ${units.enum_units(subsection)} +
+
+ +
    + % for policy_name in policy_metadata.keys(): +
  1. + +
  2. + % endfor + + + New Policy Data + +
+
+ + + +