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"/> - 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 name="bodyclass">import + +<%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.

+
+
+

Course to import:

+ Choose File +

change

+ + +
+
+
+
+ + +<%block name="jsextra"> + + + \ 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"/> - 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 name="bodyclass">users -<%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"/>
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 %> -
+<% active_tab_class = 'active-tab-' + active_tab if active_tab else '' %> +
- 6.002x Circuits and Electronics + % if context_course: + <% ctx_loc = context_course.location %> + ${context_course.display_name}
    -
  • Courseware
  • -
  • Pages
  • -
  • Assets
  • -
  • Users
  • +
  • Courseware
  • +
  • Pages
  • +
  • Assets
  • +
  • Users
  • +
  • Import
+ % endif
${ user.username } diff --git a/cms/urls.py b/cms/urls.py index 2fd4ed25e8..1c2e70b35d 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -16,8 +16,14 @@ urlpatterns = ('', 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[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.course_index', name='course_index'), + url(r'^(?P[^/]+)/(?P[^/]+)/import/(?P[^/]+)$', + 'contentstore.views.import_course', name='import_course'), + url(r'^github_service_hook$', 'github_sync.views.github_post_receive'), url(r'^preview/modx/(?P[^/]*)/(?P.*?)/(?P[^/]*)$', 'contentstore.views.preview_dispatch', name='preview_dispatch'), @@ -46,8 +52,6 @@ urlpatterns = ('', url(r'^edge$', 'contentstore.views.edge', name='edge'), url(r'^heartbeat$', include('heartbeat.urls')), - - url(r'import_course$', 'contentstore.views.import_course', name='import_course'), ) # User creation and updating views