Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into fix/cdodge/cas-thumbnail-exception-handling
This commit is contained in:
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
70
cms/static/sass/_import.scss
vendored
Normal file
70
cms/static/sass/_import.scss
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
57
cms/templates/import.html
Normal file
57
cms/templates/import.html
Normal file
@@ -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">
|
||||
<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>
|
||||
@@ -28,6 +28,4 @@
|
||||
|
||||
</section>
|
||||
|
||||
<%include file="widgets/import-course.html"/>
|
||||
|
||||
</%block>
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<%! 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
|
||||
|
||||
Reference in New Issue
Block a user