Commit 78df2660 by Chris Dodge

Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into…

Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into fix/cdodge/cas-thumbnail-exception-handling
parents b17836f2 5aa474a1
...@@ -12,6 +12,9 @@ import tarfile ...@@ -12,6 +12,9 @@ import tarfile
import shutil import shutil
from collections import defaultdict from collections import defaultdict
from uuid import uuid4 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' # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image from PIL import Image
...@@ -24,6 +27,7 @@ from django_future.csrf import ensure_csrf_cookie ...@@ -24,6 +27,7 @@ from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django import forms from django import forms
from django.shortcuts import redirect
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
...@@ -51,6 +55,7 @@ from .utils import get_course_location_for_item, get_lms_link_for_item, compute_ ...@@ -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.templates import all_templates
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml import edx_xml_parser
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -157,6 +162,8 @@ def course_index(request, org, course, name): ...@@ -157,6 +162,8 @@ def course_index(request, org, course, name):
sections = course.get_children() sections = course.get_children()
return render_to_response('overview.html', { return render_to_response('overview.html', {
'active_tab': 'courseware',
'context_course': course,
'sections': sections, 'sections': sections,
'parent_location': course.location, 'parent_location': course.location,
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'), 'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
...@@ -198,6 +205,7 @@ def edit_subsection(request, location): ...@@ -198,6 +205,7 @@ def edit_subsection(request, location):
return render_to_response('edit_subsection.html', return render_to_response('edit_subsection.html',
{'subsection': item, {'subsection': item,
'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link, 'lms_link': lms_link,
'parent_item' : parent, 'parent_item' : parent,
...@@ -256,6 +264,8 @@ def edit_unit(request, location): ...@@ -256,6 +264,8 @@ def edit_unit(request, location):
published_date = None published_date = None
return render_to_response('unit.html', { return render_to_response('unit.html', {
'context_course': item,
'active_tab': 'courseware',
'unit': item, 'unit': item,
'unit_location': location, 'unit_location': location,
'components': components, 'components': components,
...@@ -679,7 +689,11 @@ def manage_users(request, location): ...@@ -679,7 +689,11 @@ def manage_users(request, location):
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied() raise PermissionDenied()
course_module = modulestore().get_item(location)
return render_to_response('manage_users.html', { 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), 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'), 'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/') 'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/')
...@@ -755,7 +769,19 @@ def landing(request, org, course, coursename): ...@@ -755,7 +769,19 @@ def landing(request, org, course, coursename):
def static_pages(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): def edit_static(request, org, course, coursename):
...@@ -784,11 +810,14 @@ def asset_index(request, org, course, name): ...@@ -784,11 +810,14 @@ def asset_index(request, org, course, name):
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() raise PermissionDenied()
upload_asset_callback_url = reverse('upload_asset', kwargs = { upload_asset_callback_url = reverse('upload_asset', kwargs = {
'org' : org, 'org' : org,
'course' : course, 'course' : course,
'coursename' : name 'coursename' : name
}) })
course_module = modulestore().get_item(location)
course_reference = StaticContent.compute_location(org, course, name) course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference) assets = contentstore().get_all_content_for_course(course_reference)
...@@ -811,6 +840,8 @@ def asset_index(request, org, course, name): ...@@ -811,6 +840,8 @@ def asset_index(request, org, course, name):
asset_display.append(display_info) asset_display.append(display_info)
return render_to_response('asset_index.html', { return render_to_response('asset_index.html', {
'active_tab': 'assets',
'context_course': course_module,
'assets': asset_display, 'assets': asset_display,
'upload_asset_callback_url': upload_asset_callback_url 'upload_asset_callback_url': upload_asset_callback_url
}) })
...@@ -820,45 +851,69 @@ def asset_index(request, org, course, name): ...@@ -820,45 +851,69 @@ def asset_index(request, org, course, name):
def edge(request): def edge(request):
return render_to_response('university_profiles/edge.html', {}) return render_to_response('university_profiles/edge.html', {})
def import_course(request): @ensure_csrf_cookie
if request.method != 'POST': @login_required
# (cdodge) @todo: Is there a way to do a - say - 'raise Http400'? def import_course(request, org, course, name):
return HttpResponseBadRequest()
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'): if not filename.endswith('.tar.gz'):
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'})) 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 logging.debug('importing course to {0}'.format(temp_filepath))
temp_file = open(temp_filepath, 'wb+')
for chunk in request.FILES['file'].chunks():
temp_file.write(chunk)
temp_file.close()
tf = tarfile.open(temp_filepath) # stream out the uploaded files in chunks to disk
tf.extractall(settings.GITHUB_REPO_ROOT + '/') 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 :-) with open(data_root / course_dir / 'course.xml', 'r') as course_file:
shutil.rmtree(temp_filepath.replace('.tar.gz', '')) 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() { ...@@ -51,8 +51,22 @@ $(document).ready(function() {
$('.remove-policy-data').bind('click', removePolicyMetadata); $('.remove-policy-data').bind('click', removePolicyMetadata);
$('.sync-date').bind('click', syncReleaseDate); $('.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) { function syncReleaseDate(e) {
e.preventDefault(); e.preventDefault();
$("#start_date").val(""); $("#start_date").val("");
......
...@@ -119,6 +119,13 @@ label { ...@@ -119,6 +119,13 @@ label {
font-size: 12px; font-size: 12px;
} }
code {
padding: 0 4px;
border-radius: 3px;
background: #eee;
font-family: Monaco, monospace;
}
.text-editor { .text-editor {
width: 100%; width: 100%;
min-height: 80px; min-height: 80px;
......
...@@ -4,6 +4,11 @@ body.no-header { ...@@ -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 { .primary-header {
width: 100%; width: 100%;
height: 36px; height: 36px;
...@@ -13,6 +18,30 @@ body.no-header { ...@@ -13,6 +18,30 @@ body.no-header {
color: #fff; color: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset); @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 { .left {
width: 700px; width: 700px;
} }
...@@ -48,9 +77,5 @@ body.no-header { ...@@ -48,9 +77,5 @@ body.no-header {
background: rgba(255, 255, 255, .1); 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 @@ ...@@ -16,6 +16,7 @@
@import "assets"; @import "assets";
@import "static-pages"; @import "static-pages";
@import "users"; @import "users";
@import "import";
@import "course-info"; @import "course-info";
@import "landing"; @import "landing";
@import "graphics"; @import "graphics";
......
...@@ -18,8 +18,7 @@ ...@@ -18,8 +18,7 @@
</head> </head>
<body class="<%block name='bodyclass'></%block>"> <body class="<%block name='bodyclass'></%block>">
<%include file="widgets/header.html" args="active_tab=active_tab"/>
<%include file="widgets/header.html"/>
<%include file="courseware_vendor_js.html"/> <%include file="courseware_vendor_js.html"/>
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
......
...@@ -10,7 +10,5 @@ ...@@ -10,7 +10,5 @@
<section class="main-content"> <section class="main-content">
</section> </section>
<%include file="widgets/upload_assets.html"/>
</section> </section>
</%block> </%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 @@ ...@@ -28,6 +28,4 @@
</section> </section>
<%include file="widgets/import-course.html"/>
</%block> </%block>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Course Staff Manager</%block> <%block name="title">Course Staff Manager</%block>
<%block name="bodyclass">users</%block> <%block name="bodyclass">users</%block>
<%include file="widgets/header.html"/>
<%block name="content"> <%block name="content">
<div class="main-wrapper"> <div class="main-wrapper">
......
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
<a href="#" class="drag-handle wip"></a> <a href="#" class="drag-handle wip"></a>
</div> </div>
</div> </div>
${units.enum_units(subsection)} ${units.enum_units(subsection)}
</li> </li>
% endfor % endfor
</ol> </ol>
...@@ -92,7 +92,6 @@ ...@@ -92,7 +92,6 @@
% endfor % endfor
</article> </article>
</div> </div>
<%include file="widgets/upload_assets.html"/>
</div> </div>
<footer></footer> <footer></footer>
</%block> </%block>
<%! from django.core.urlresolvers import reverse %> <%! 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"> <nav class="inner-wrapper">
<div class="left"> <div class="left">
<a href="/"><span class="home-icon"></span></a> <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"> <ul class="class-nav">
<li><a href="#" class="active">Courseware</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="#" class="wip-box">Pages</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="#" class="wip-box">Assets</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="#" class="wip-box">Users</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> </ul>
% endif
</div> </div>
<div class="right"> <div class="right">
<span class="username">${ user.username }</span> <span class="username">${ user.username }</span>
......
...@@ -16,8 +16,14 @@ urlpatterns = ('', ...@@ -16,8 +16,14 @@ urlpatterns = ('',
url(r'^create_draft$', 'contentstore.views.create_draft', name='create_draft'), url(r'^create_draft$', 'contentstore.views.create_draft', name='create_draft'),
url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_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'^unpublish_unit$', 'contentstore.views.unpublish_unit', name='unpublish_unit'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
'contentstore.views.course_index', name='course_index'), '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'^github_service_hook$', 'github_sync.views.github_post_receive'),
url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$', url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'contentstore.views.preview_dispatch', name='preview_dispatch'), 'contentstore.views.preview_dispatch', name='preview_dispatch'),
...@@ -46,8 +52,6 @@ urlpatterns = ('', ...@@ -46,8 +52,6 @@ urlpatterns = ('',
url(r'^edge$', 'contentstore.views.edge', name='edge'), url(r'^edge$', 'contentstore.views.edge', name='edge'),
url(r'^heartbeat$', include('heartbeat.urls')), url(r'^heartbeat$', include('heartbeat.urls')),
url(r'import_course$', 'contentstore.views.import_course', name='import_course'),
) )
# User creation and updating views # 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