diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 85d0253a7e..6375f90d4e 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -12,6 +12,9 @@ import tarfile
import shutil
from collections import defaultdict
from uuid import uuid4
+from lxml import etree
+from path import path
+from shutil import rmtree
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image
@@ -24,6 +27,7 @@ from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.conf import settings
from django import forms
+from django.shortcuts import redirect
from xmodule.modulestore import Location
from xmodule.x_module import ModuleSystem
@@ -51,6 +55,7 @@ from .utils import get_course_location_for_item, get_lms_link_for_item, compute_
from xmodule.templates import all_templates
from xmodule.modulestore.xml_importer import import_from_xml
+from xmodule.modulestore.xml import edx_xml_parser
log = logging.getLogger(__name__)
@@ -157,6 +162,8 @@ def course_index(request, org, course, name):
sections = course.get_children()
return render_to_response('overview.html', {
+ 'active_tab': 'courseware',
+ 'context_course': course,
'sections': sections,
'parent_location': course.location,
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
@@ -198,6 +205,7 @@ def edit_subsection(request, location):
return render_to_response('edit_subsection.html',
{'subsection': item,
+ 'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link,
'parent_item' : parent,
@@ -256,6 +264,8 @@ def edit_unit(request, location):
published_date = None
return render_to_response('unit.html', {
+ 'context_course': item,
+ 'active_tab': 'courseware',
'unit': item,
'unit_location': location,
'components': components,
@@ -679,7 +689,11 @@ def manage_users(request, location):
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
+ course_module = modulestore().get_item(location)
+
return render_to_response('manage_users.html', {
+ 'active_tab': 'users',
+ 'context_course': course_module,
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/')
@@ -755,7 +769,19 @@ def landing(request, org, course, coursename):
def static_pages(request, org, course, coursename):
- return render_to_response('static-pages.html', {})
+
+ location = ['i4x', org, course, 'course', coursename]
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ course = modulestore().get_item(location)
+
+ return render_to_response('static-pages.html', {
+ 'active_tab': 'pages',
+ 'context_course': course,
+ })
def edit_static(request, org, course, coursename):
@@ -784,11 +810,14 @@ def asset_index(request, org, course, name):
if not has_access(request.user, location):
raise PermissionDenied()
+
upload_asset_callback_url = reverse('upload_asset', kwargs = {
'org' : org,
'course' : course,
'coursename' : name
})
+
+ course_module = modulestore().get_item(location)
course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference)
@@ -811,6 +840,8 @@ def asset_index(request, org, course, name):
asset_display.append(display_info)
return render_to_response('asset_index.html', {
+ 'active_tab': 'assets',
+ 'context_course': course_module,
'assets': asset_display,
'upload_asset_callback_url': upload_asset_callback_url
})
@@ -820,45 +851,69 @@ def asset_index(request, org, course, name):
def edge(request):
return render_to_response('university_profiles/edge.html', {})
-def import_course(request):
- if request.method != 'POST':
- # (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
- return HttpResponseBadRequest()
+@ensure_csrf_cookie
+@login_required
+def import_course(request, org, course, name):
- filename = request.FILES['file'].name
+ location = ['i4x', org, course, 'course', name]
- if not filename.endswith('.tar.gz'):
- return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
- temp_filepath = settings.GITHUB_REPO_ROOT + '/' + filename
+ if request.method == 'POST':
+ filename = request.FILES['course-data'].name
- logging.debug('importing course to {0}'.format(temp_filepath))
+ if not filename.endswith('.tar.gz'):
+ return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
- # stream out the uploaded files in chunks to disk
- temp_file = open(temp_filepath, 'wb+')
- for chunk in request.FILES['file'].chunks():
- temp_file.write(chunk)
- temp_file.close()
+ data_root = path(settings.GITHUB_REPO_ROOT)
- tf = tarfile.open(temp_filepath)
- tf.extractall(settings.GITHUB_REPO_ROOT + '/')
+ temp_filepath = data_root / filename
- os.remove(temp_filepath) # remove the .tar.gz file
+ logging.debug('importing course to {0}'.format(temp_filepath))
- # @todo: don't assume the top-level directory that was unziped was the same name (but without .tar.gz)
+ # stream out the uploaded files in chunks to disk
+ temp_file = open(temp_filepath, 'wb+')
+ for chunk in request.FILES['course-data'].chunks():
+ temp_file.write(chunk)
+ temp_file.close()
- course_dir = filename.replace('.tar.gz','')
+ # @todo: don't assume the top-level directory that was unziped was the same name (but without .tar.gz)
+ course_dir = filename.replace('.tar.gz', '')
- module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
- [course_dir], load_error_modules=False,static_content_store=contentstore())
+ tf = tarfile.open(temp_filepath)
+ shutil.rmtree(data_root / course_dir)
+ tf.extractall(data_root + '/')
- # remove content directory - we *shouldn't* need this any longer :-)
- shutil.rmtree(temp_filepath.replace('.tar.gz', ''))
-
- logging.debug('new course at {0}'.format(course_items[0].location))
-
- create_all_course_groups(request.user, course_items[0].location)
+ os.remove(temp_filepath) # remove the .tar.gz file
+ with open(data_root / course_dir / 'course.xml', 'r') as course_file:
+ course_data = etree.parse(course_file, parser=edx_xml_parser)
+ course_data_root = course_data.getroot()
+ course_data_root.set('org', org)
+ course_data_root.set('course', course)
+ course_data_root.set('url_name', name)
- return HttpResponse(json.dumps({'Status' : 'OK'}))
+ with open(data_root / course_dir / 'course.xml', 'w') as course_file:
+ course_data.write(course_file)
+
+ module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
+ [course_dir], load_error_modules=False, static_content_store=contentstore())
+
+ # remove content directory - we *shouldn't* need this any longer :-)
+ shutil.rmtree(data_root / course_dir)
+
+ logging.debug('new course at {0}'.format(course_items[0].location))
+
+ create_all_course_groups(request.user, course_items[0].location)
+
+ return HttpResponse(json.dumps({'Status': 'OK'}))
+ else:
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('import.html', {
+ 'context_course': course_module,
+ 'active_tab': 'import',
+ })
diff --git a/cms/static/js/base.js b/cms/static/js/base.js
index 9cb09d733c..8602770d24 100644
--- a/cms/static/js/base.js
+++ b/cms/static/js/base.js
@@ -51,8 +51,22 @@ $(document).ready(function() {
$('.remove-policy-data').bind('click', removePolicyMetadata);
$('.sync-date').bind('click', syncReleaseDate);
+
+ // import form setup
+ $('.import .file-input').bind('change', showImportSubmit);
+ $('.import .choose-file-button, .import .choose-file-button-inline').bind('click', function(e) {
+ e.preventDefault();
+ $('.import .file-input').click();
+ });
});
+function showImportSubmit(e) {
+ $('.file-name').html($(this).val())
+ $('.file-name-block').show();
+ $('.import .choose-file-button').hide();
+ $('.submit-button').show();
+}
+
function syncReleaseDate(e) {
e.preventDefault();
$("#start_date").val("");
diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss
index 05e40c9483..69b51b727e 100644
--- a/cms/static/sass/_base.scss
+++ b/cms/static/sass/_base.scss
@@ -119,6 +119,13 @@ label {
font-size: 12px;
}
+code {
+ padding: 0 4px;
+ border-radius: 3px;
+ background: #eee;
+ font-family: Monaco, monospace;
+}
+
.text-editor {
width: 100%;
min-height: 80px;
diff --git a/cms/static/sass/_header.scss b/cms/static/sass/_header.scss
index d70f53b4df..5255819a60 100644
--- a/cms/static/sass/_header.scss
+++ b/cms/static/sass/_header.scss
@@ -4,6 +4,11 @@ body.no-header {
}
}
+@mixin active {
+ @include linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3));
+ @include box-shadow(0 2px 8px rgba(0, 0, 0, .7) inset);
+}
+
.primary-header {
width: 100%;
height: 36px;
@@ -13,6 +18,30 @@ 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);
+ &.active-tab-courseware #courseware-tab {
+ @include active;
+ }
+
+ &.active-tab-assets #assets-tab {
+ @include active;
+ }
+
+ &.active-tab-pages #pages-tab {
+ @include active;
+ }
+
+ &.active-tab-users #users-tab {
+ @include active;
+ }
+
+ &.active-tab-import #import-tab {
+ @include active;
+ }
+
+ #import-tab {
+ @include box-shadow(1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44);
+ }
+
.left {
width: 700px;
}
@@ -48,9 +77,5 @@ body.no-header {
background: rgba(255, 255, 255, .1);
}
- &.active {
- @include linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3));
- @include box-shadow(0 2px 8px rgba(0, 0, 0, .7) inset);
- }
}
}
\ No newline at end of file
diff --git a/cms/static/sass/_import.scss b/cms/static/sass/_import.scss
new file mode 100644
index 0000000000..f9480a6d46
--- /dev/null
+++ b/cms/static/sass/_import.scss
@@ -0,0 +1,70 @@
+.import {
+ .import-overview {
+ @extend .window;
+ @include clearfix;
+ padding: 30px 40px;
+ }
+
+ .description {
+ float: left;
+ width: 62%;
+ margin-right: 3%;
+ font-size: 14px;
+
+ h3 {
+ margin-bottom: 20px;
+ font-size: 18px;
+ font-weight: 700;
+ color: $error-red;
+ }
+
+ p + p {
+ margin-top: 20px;
+ }
+ }
+
+ .import-form {
+ float: left;
+ width: 35%;
+ padding: 25px 30px 35px;
+ @include box-sizing(border-box);
+ border: 1px solid $mediumGrey;
+ border-radius: 3px;
+ background: $lightGrey;
+ text-align: center;
+
+ h2 {
+ margin-bottom: 30px;
+ font-size: 26px;
+ font-weight: 300;
+ }
+
+ .file-name-block {
+ display: none;
+ margin-bottom: 15px;
+ font-size: 13px;
+ }
+
+ .choose-file-button {
+ @include blue-button;
+ padding: 10px 50px 11px;
+ font-size: 17px;
+ }
+
+ .choose-file-button-inline {
+ display: block;
+ }
+
+ .file-input {
+ display: none;
+ }
+
+ .submit-button {
+ @include orange-button;
+ display: none;
+ max-width: 100%;
+ padding: 8px 20px 10px;
+ white-space: normal;
+ }
+ }
+}
\ No newline at end of file
diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss
index f2256f97ed..0824cc380d 100644
--- a/cms/static/sass/base-style.scss
+++ b/cms/static/sass/base-style.scss
@@ -16,6 +16,7 @@
@import "assets";
@import "static-pages";
@import "users";
+@import "import";
@import "course-info";
@import "landing";
@import "graphics";
diff --git a/cms/templates/base.html b/cms/templates/base.html
index f847ad6f7b..f839cb9753 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -18,8 +18,7 @@
-
- <%include file="widgets/header.html"/>
+ <%include file="widgets/header.html" args="active_tab=active_tab"/>
<%include file="courseware_vendor_js.html"/>
diff --git a/cms/templates/course_index.html b/cms/templates/course_index.html
index 37b5a8b371..e490ad7817 100644
--- a/cms/templates/course_index.html
+++ b/cms/templates/course_index.html
@@ -10,7 +10,5 @@
- <%include file="widgets/upload_assets.html"/>
-
%block>
diff --git a/cms/templates/import.html b/cms/templates/import.html
new file mode 100644
index 0000000000..16e1353870
--- /dev/null
+++ b/cms/templates/import.html
@@ -0,0 +1,57 @@
+<%inherit file="base.html" />
+<%! from django.core.urlresolvers import reverse %>
+<%block name="title">Import%block>
+<%block name="bodyclass">import%block>
+
+<%block name="content">
+
+
+
Import
+
+
+
Importing a new course will delete all course content currently associated with your course
+ and replace it with the contents of the uploaded file.
+
File uploads must be zip files containing, at a minimum, a course.xml file.
+
Please note that if your course has any problems with auto-generated url_name nodes,
+ re-importing your course could cause the loss of student data associated with those problems.
+
+
+
+
+
+%block>
+
+<%block name="jsextra">
+
+
+%block>
\ No newline at end of file
diff --git a/cms/templates/index.html b/cms/templates/index.html
index e63bbbb84d..4b721a0865 100644
--- a/cms/templates/index.html
+++ b/cms/templates/index.html
@@ -28,6 +28,4 @@
-<%include file="widgets/import-course.html"/>
-
%block>
diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html
index e479bc0942..142afc2304 100644
--- a/cms/templates/manage_users.html
+++ b/cms/templates/manage_users.html
@@ -1,7 +1,6 @@
<%inherit file="base.html" />
<%block name="title">Course Staff Manager%block>
<%block name="bodyclass">users%block>
-<%include file="widgets/header.html"/>
<%block name="content">
diff --git a/cms/templates/overview.html b/cms/templates/overview.html
index d31e1e4823..eb114b785f 100644
--- a/cms/templates/overview.html
+++ b/cms/templates/overview.html
@@ -83,7 +83,7 @@
- ${units.enum_units(subsection)}
+ ${units.enum_units(subsection)}
% endfor
@@ -92,7 +92,6 @@
% endfor
- <%include file="widgets/upload_assets.html"/>
%block>
diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html
index fb436ddde2..08e998381a 100644
--- a/cms/templates/widgets/header.html
+++ b/cms/templates/widgets/header.html
@@ -1,16 +1,21 @@
<%! from django.core.urlresolvers import reverse %>
-