Commit 5aa474a1 by chrisndodge

Merge pull request #879 from MITx/feature/cale/cas-import-ui

Feature/cale/cas import ui
parents 9cf5cd08 c70f29e8
......@@ -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]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
if request.method == 'POST':
filename = request.FILES['course-data'].name
if not filename.endswith('.tar.gz'):
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
if not filename.endswith('.tar.gz'):
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
temp_filepath = settings.GITHUB_REPO_ROOT + '/' + filename
data_root = path(settings.GITHUB_REPO_ROOT)
logging.debug('importing course to {0}'.format(temp_filepath))
temp_filepath = data_root / filename
# 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()
logging.debug('importing course to {0}'.format(temp_filepath))
tf = tarfile.open(temp_filepath)
tf.extractall(settings.GITHUB_REPO_ROOT + '/')
# 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()
os.remove(temp_filepath) # remove the .tar.gz file
# @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', '')
# @todo: don't assume the top-level directory that was unziped was the same name (but without .tar.gz)
tf = tarfile.open(temp_filepath)
shutil.rmtree(data_root / course_dir)
tf.extractall(data_root + '/')
course_dir = filename.replace('.tar.gz','')
os.remove(temp_filepath) # remove the .tar.gz 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(temp_filepath.replace('.tar.gz', ''))
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)
logging.debug('new course at {0}'.format(course_items[0].location))
with open(data_root / course_dir / 'course.xml', 'w') as course_file:
course_data.write(course_file)
create_all_course_groups(request.user, course_items[0].location)
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 HttpResponse(json.dumps({'Status' : 'OK'}))
return render_to_response('import.html', {
'context_course': course_module,
'active_tab': 'import',
})
......@@ -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("");
......
......@@ -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;
......
......@@ -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
.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
......@@ -16,6 +16,7 @@
@import "assets";
@import "static-pages";
@import "users";
@import "import";
@import "course-info";
@import "landing";
@import "graphics";
......
......@@ -18,8 +18,7 @@
</head>
<body class="<%block name='bodyclass'></%block>">
<%include file="widgets/header.html"/>
<%include file="widgets/header.html" args="active_tab=active_tab"/>
<%include file="courseware_vendor_js.html"/>
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
......
......@@ -10,7 +10,5 @@
<section class="main-content">
</section>
<%include file="widgets/upload_assets.html"/>
</section>
</%block>
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Import</%block>
<%block name="bodyclass">import</%block>
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Import</h1>
<article class="import-overview">
<div class="description">
<h3>Importing a new course will delete all course content currently associated with your course
and replace it with the contents of the uploaded file.</h3>
<p>File uploads must be zip files containing, at a minimum, a <code>course.xml</code> file.</p>
<p>Please note that if your course has any problems with auto-generated <code>url_name</code> nodes,
re-importing your course could cause the loss of student data associated with those problems.</p>
</div>
<form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form">
<h2>Course to import:</h2>
<a href="#" class="choose-file-button">Choose File</a>
<p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">change</a></p>
<input type="file" name="course-data" class="file-input">
<input type="submit" value="Replace my course with the one above" class="submit-button">
</form>
</article>
</div>
</div>
</%block>
<%block name="jsextra">
<script src="http://malsup.github.com/jquery.form.js"></script>
<script>
(function() {
var bar = $('.bar');
var percent = $('.percent');
var status = $('#status');
$('form').ajaxForm({
beforeSend: function() {
status.empty();
var percentVal = '0%';
bar.width(percentVal)
percent.html(percentVal);
},
uploadProgress: function(event, position, total, percentComplete) {
var percentVal = percentComplete + '%';
bar.width(percentVal)
percent.html(percentVal);
},
complete: function(xhr) {
status.html(xhr.responseText);
}
});
})();
</script>
</%block>
\ No newline at end of file
......@@ -28,6 +28,4 @@
</section>
<%include file="widgets/import-course.html"/>
</%block>
<%inherit file="base.html" />
<%block name="title">Course Staff Manager</%block>
<%block name="bodyclass">users</%block>
<%include file="widgets/header.html"/>
<%block name="content">
<div class="main-wrapper">
......
......@@ -83,7 +83,7 @@
<a href="#" class="drag-handle wip"></a>
</div>
</div>
${units.enum_units(subsection)}
${units.enum_units(subsection)}
</li>
% endfor
</ol>
......@@ -92,7 +92,6 @@
% endfor
</article>
</div>
<%include file="widgets/upload_assets.html"/>
</div>
<footer></footer>
</%block>
<%! from django.core.urlresolvers import reverse %>
<header class="primary-header">
<% active_tab_class = 'active-tab-' + active_tab if active_tab else '' %>
<header class="primary-header ${active_tab_class}">
<nav class="inner-wrapper">
<div class="left">
<a href="/"><span class="home-icon"></span></a>
<a href="#" class="class-name wip-box">6.002x Circuits and Electronics</a>
% if context_course:
<% ctx_loc = context_course.location %>
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" class="class-name">${context_course.display_name}</a>
<ul class="class-nav">
<li><a href="#" class="active">Courseware</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>
<li><a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='courseware-tab'>Courseware</a></li>
<li><a href="${reverse('static_pages', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}" id='pages-tab'>Pages</a></li>
<li><a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='assets-tab'>Assets</a></li>
<li><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}" id='users-tab'>Users</a></li>
<li><a href="${reverse('import_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='import-tab' class="wip-box">Import</a></li>
</ul>
% endif
</div>
<div class="right">
<span class="username">${ user.username }</span>
......
......@@ -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<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
'contentstore.views.course_index', name='course_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/import/(?P<name>[^/]+)$',
'contentstore.views.import_course', name='import_course'),
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'),
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment