Merge branch 'master' into tests/diana/update-oe-unit-tests
This commit is contained in:
@@ -8,6 +8,8 @@ from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
from tempfile import mkdtemp
|
||||
import json
|
||||
from fs.osfs import OSFS
|
||||
|
||||
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
@@ -350,6 +352,33 @@ class ContentStoreTest(TestCase):
|
||||
def test_edit_unit_full(self):
|
||||
self.check_edit_unit('full')
|
||||
|
||||
def test_static_tab_reordering(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
|
||||
|
||||
# reverse the ordering
|
||||
reverse_tabs = []
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||
|
||||
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs':reverse_tabs}), "application/json")
|
||||
|
||||
course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
|
||||
|
||||
# compare to make sure that the tabs information is in the expected order after the server call
|
||||
course_tabs = []
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
course_tabs.append('i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||
|
||||
self.assertEqual(reverse_tabs, course_tabs)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_about_overrides(self):
|
||||
'''
|
||||
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
|
||||
@@ -430,6 +459,28 @@ class ContentStoreTest(TestCase):
|
||||
# export out to a tempdir
|
||||
export_to_xml(ms, cs, location, root_dir, 'test_export')
|
||||
|
||||
# check for static tabs
|
||||
fs = OSFS(root_dir / 'test_export')
|
||||
self.assertTrue(fs.exists('tabs'))
|
||||
|
||||
static_tabs_query_loc = Location('i4x', location.org, location.course, 'static_tab', None)
|
||||
static_tabs = ms.get_items(static_tabs_query_loc)
|
||||
|
||||
for static_tab in static_tabs:
|
||||
fs = OSFS(root_dir / 'test_export/tabs')
|
||||
self.assertTrue(fs.exists(static_tab.location.name + '.html'))
|
||||
|
||||
# check for custom_tags
|
||||
fs = OSFS(root_dir / 'test_export')
|
||||
self.assertTrue(fs.exists('custom_tags'))
|
||||
|
||||
custom_tags_query_loc = Location('i4x', location.org, location.course, 'custom_tag_template', None)
|
||||
custom_tags = ms.get_items(custom_tags_query_loc)
|
||||
|
||||
for custom_tag in custom_tags:
|
||||
fs = OSFS(root_dir / 'test_export/custom_tags')
|
||||
self.assertTrue(fs.exists(custom_tag.location.name))
|
||||
|
||||
# remove old course
|
||||
delete_course(ms, cs, location)
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ def has_access(user, location, role=STAFF_ROLE_NAME):
|
||||
Return True if user allowed to access this piece of data
|
||||
Note that the CMS permissions model is with respect to courses
|
||||
There is a super-admin permissions if user.is_staff is set
|
||||
Also, since we're unifying the user database between LMS and CAS,
|
||||
Also, since we're unifying the user database between LMS and CAS,
|
||||
I'm presuming that the course instructor (formally known as admin)
|
||||
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
|
||||
has all the rights that STAFF do
|
||||
@@ -154,7 +154,7 @@ def course_index(request, org, course, name):
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
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()
|
||||
@@ -213,7 +213,7 @@ def edit_subsection(request, location):
|
||||
|
||||
# remove all metadata from the generic dictionary that is presented in a more normalized UI
|
||||
|
||||
policy_metadata = dict((key,value) for key, value in item.metadata.iteritems()
|
||||
policy_metadata = dict((key,value) for key, value in item.metadata.iteritems()
|
||||
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
|
||||
|
||||
can_view_live = False
|
||||
@@ -292,7 +292,7 @@ def edit_unit(request, location):
|
||||
containing_section = modulestore().get_item(containing_section_locs[0])
|
||||
|
||||
# cdodge hack. We're having trouble previewing drafts via jump_to redirect
|
||||
# so let's generate the link url here
|
||||
# so let's generate the link url here
|
||||
|
||||
# need to figure out where this item is in the list of children as the preview will need this
|
||||
index =1
|
||||
@@ -303,12 +303,12 @@ def edit_unit(request, location):
|
||||
|
||||
preview_lms_link = '//{preview}{lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
||||
preview='preview.',
|
||||
lms_base=settings.LMS_BASE,
|
||||
lms_base=settings.LMS_BASE,
|
||||
org=course.location.org,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
index=index)
|
||||
|
||||
unit_state = compute_unit_state(item)
|
||||
@@ -359,14 +359,14 @@ def assignment_type_update(request, org, course, category, name):
|
||||
location = Location(['i4x', org, course, category, name])
|
||||
if not has_access(request.user, location):
|
||||
raise HttpResponseForbidden()
|
||||
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
||||
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
|
||||
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
|
||||
def user_author_string(user):
|
||||
'''Get an author string for commits by this user. Format:
|
||||
@@ -511,23 +511,23 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
|
||||
error_msg=exc_info_to_str(sys.exc_info())
|
||||
).xmodule_constructor(system)(None, None)
|
||||
|
||||
# cdodge: Special case
|
||||
# cdodge: Special case
|
||||
if module.location.category == 'static_tab':
|
||||
module.get_html = wrap_xmodule(
|
||||
module.get_html,
|
||||
module,
|
||||
"xmodule_tab_display.html",
|
||||
)
|
||||
else:
|
||||
else:
|
||||
module.get_html = wrap_xmodule(
|
||||
module.get_html,
|
||||
module,
|
||||
"xmodule_display.html",
|
||||
)
|
||||
|
||||
|
||||
module.get_html = replace_static_urls(
|
||||
module.get_html,
|
||||
module.metadata.get('data_dir', module.location.course),
|
||||
'/static/' + module.metadata.get('data_dir', module.location.course),
|
||||
course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None])
|
||||
)
|
||||
save_preview_state(request, preview_id, descriptor.location.url(),
|
||||
@@ -555,7 +555,7 @@ def _xmodule_recurse(item, action):
|
||||
_xmodule_recurse(child, action)
|
||||
|
||||
action(item)
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
@@ -590,7 +590,7 @@ def delete_item(request):
|
||||
# delete_item on a vertical tries to delete the draft version leaving the
|
||||
# requested delete to never occur
|
||||
if item.location.revision is None and item.location.category=='vertical' and delete_all_versions:
|
||||
modulestore('direct').delete_item(item.location)
|
||||
modulestore('direct').delete_item(item.location)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
@@ -609,7 +609,7 @@ def save_item(request):
|
||||
if request.POST.get('data') is not None:
|
||||
data = request.POST['data']
|
||||
store.update_item(item_location, data)
|
||||
|
||||
|
||||
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
|
||||
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
|
||||
# deleting the children object from the children collection
|
||||
@@ -699,7 +699,7 @@ def unpublish_unit(request):
|
||||
def clone_item(request):
|
||||
parent_location = Location(request.POST['parent_location'])
|
||||
template = Location(request.POST['template'])
|
||||
|
||||
|
||||
display_name = request.POST.get('display_name')
|
||||
|
||||
if not has_access(request.user, parent_location):
|
||||
@@ -739,9 +739,9 @@ def upload_asset(request, org, course, coursename):
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
if not has_access(request.user, location):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
|
||||
# Does the course actually exist?!? Get anything from it to prove its existance
|
||||
|
||||
|
||||
try:
|
||||
item = modulestore().get_item(location)
|
||||
except:
|
||||
@@ -775,9 +775,9 @@ def upload_asset(request, org, course, coursename):
|
||||
|
||||
# readback the saved content - we need the database timestamp
|
||||
readback = contentstore().find(content.location)
|
||||
|
||||
response_payload = {'displayname' : content.name,
|
||||
'uploadDate' : get_date_display(readback.last_modified_at),
|
||||
|
||||
response_payload = {'displayname' : content.name,
|
||||
'uploadDate' : get_date_display(readback.last_modified_at),
|
||||
'url' : StaticContent.get_url_path_from_location(content.location),
|
||||
'thumb_url' : StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
||||
'msg' : 'Upload completed'
|
||||
@@ -793,7 +793,7 @@ This view will return all CMS users who are editors for the specified course
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def manage_users(request, location):
|
||||
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
@@ -809,7 +809,7 @@ def manage_users(request, location):
|
||||
'allow_actions' : has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
|
||||
'request_user_id' : request.user.id
|
||||
})
|
||||
|
||||
|
||||
|
||||
def create_json_response(errmsg = None):
|
||||
if errmsg is not None:
|
||||
@@ -831,13 +831,13 @@ def add_user(request, location):
|
||||
|
||||
if email=='':
|
||||
return create_json_response('Please specify an email address.')
|
||||
|
||||
|
||||
# check that logged in user has admin permissions to this course
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
user = get_user_by_email(email)
|
||||
|
||||
|
||||
# user doesn't exist?!? Return error.
|
||||
if user is None:
|
||||
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
|
||||
@@ -860,7 +860,7 @@ the specified course
|
||||
@ensure_csrf_cookie
|
||||
def remove_user(request, location):
|
||||
email = request.POST["email"]
|
||||
|
||||
|
||||
# check that logged in user has admin permissions on this course
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
@@ -887,7 +887,7 @@ def landing(request, org, course, coursename):
|
||||
def static_pages(request, org, course, coursename):
|
||||
|
||||
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()
|
||||
@@ -903,10 +903,56 @@ def static_pages(request, org, course, coursename):
|
||||
def edit_static(request, org, course, coursename):
|
||||
return render_to_response('edit-static-page.html', {})
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
def reorder_static_tabs(request):
|
||||
tabs = request.POST['tabs']
|
||||
course = get_course_for_item(tabs[0])
|
||||
|
||||
if not has_access(request.user, course.location):
|
||||
raise PermissionDenied()
|
||||
|
||||
# get list of existing static tabs in course
|
||||
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
|
||||
# that we know about) otherwise we can drop some!
|
||||
|
||||
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
|
||||
if len(existing_static_tabs) != len(tabs):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# load all reference tabs, return BadRequest if we can't find any of them
|
||||
tab_items =[]
|
||||
for tab in tabs:
|
||||
item = modulestore('direct').get_item(Location(tab))
|
||||
if item is None:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
tab_items.append(item)
|
||||
|
||||
# now just go through the existing course_tabs and re-order the static tabs
|
||||
reordered_tabs = []
|
||||
static_tab_idx = 0
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reordered_tabs.append({'type': 'static_tab',
|
||||
'name' : tab_items[static_tab_idx].metadata.get('display_name'),
|
||||
'url_slug' : tab_items[static_tab_idx].location.name})
|
||||
static_tab_idx += 1
|
||||
else:
|
||||
reordered_tabs.append(tab)
|
||||
|
||||
|
||||
# OK, re-assemble the static tabs in the new order
|
||||
course.tabs = reordered_tabs
|
||||
modulestore('direct').update_metadata(course.location, course.metadata)
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def edit_tabs(request, org, course, coursename):
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
course_item = modulestore().get_item(location)
|
||||
static_tabs_loc = Location('i4x', org, course, 'static_tab', None)
|
||||
|
||||
@@ -914,12 +960,19 @@ def edit_tabs(request, org, course, coursename):
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
static_tabs = modulestore('direct').get_items(static_tabs_loc)
|
||||
|
||||
# see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
|
||||
if course_item.tabs is None or len(course_item.tabs) == 0:
|
||||
initialize_course_tabs(course_item)
|
||||
|
||||
# first get all static tabs from the tabs list
|
||||
# we do this because this is also the order in which items are displayed in the LMS
|
||||
static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
|
||||
|
||||
static_tabs = []
|
||||
for static_tab_ref in static_tabs_refs:
|
||||
static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
|
||||
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
|
||||
|
||||
components = [
|
||||
static_tab.location.url()
|
||||
for static_tab
|
||||
@@ -928,7 +981,7 @@ def edit_tabs(request, org, course, coursename):
|
||||
|
||||
return render_to_response('edit-tabs.html', {
|
||||
'active_tab': 'pages',
|
||||
'context_course':course_item,
|
||||
'context_course':course_item,
|
||||
'components': components
|
||||
})
|
||||
|
||||
@@ -949,13 +1002,13 @@ def course_info(request, org, course, name, provided_id=None):
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
|
||||
# get current updates
|
||||
location = ['i4x', org, course, 'course_info', "updates"]
|
||||
|
||||
@@ -966,7 +1019,7 @@ def course_info(request, org, course, name, provided_id=None):
|
||||
'course_updates' : json.dumps(get_course_updates(location)),
|
||||
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
|
||||
})
|
||||
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@@ -980,7 +1033,7 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
# ??? No way to check for access permission afaik
|
||||
# get current updates
|
||||
location = ['i4x', org, course, 'course_info', "updates"]
|
||||
|
||||
|
||||
# Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
|
||||
# Possibly due to my removing the seemingly redundant pattern in urls.py
|
||||
if provided_id == '':
|
||||
@@ -995,7 +1048,7 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
|
||||
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
|
||||
@@ -1012,7 +1065,7 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
@ensure_csrf_cookie
|
||||
def module_info(request, module_location):
|
||||
location = Location(module_location)
|
||||
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
@@ -1025,10 +1078,10 @@ def module_info(request, module_location):
|
||||
|
||||
rewrite_static_links = request.GET.get('rewrite_url_links','True') in ['True', 'true']
|
||||
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links','False'), rewrite_static_links))
|
||||
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
raise PermissionDenied()
|
||||
|
||||
if real_method == 'GET':
|
||||
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
|
||||
@@ -1046,20 +1099,20 @@ def get_course_settings(request, org, course, name):
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
course_details = CourseDetails.fetch(location)
|
||||
|
||||
|
||||
return render_to_response('settings.html', {
|
||||
'active_tab': 'settings',
|
||||
'active_tab': 'settings',
|
||||
'context_course': course_module,
|
||||
'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
|
||||
})
|
||||
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@@ -1082,13 +1135,13 @@ def course_settings_updates(request, org, course, name, section):
|
||||
elif section == 'grading':
|
||||
manager = CourseGradingModel
|
||||
else: return
|
||||
|
||||
|
||||
if request.method == 'GET':
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseSettingsEncoder),
|
||||
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseSettingsEncoder),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
|
||||
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
|
||||
mimetype="application/json")
|
||||
|
||||
@expect_json
|
||||
@@ -1101,7 +1154,7 @@ def course_grader_updates(request, org, course, name, grader_index=None):
|
||||
|
||||
org, course: Attributes of the Location for the item to edit
|
||||
"""
|
||||
|
||||
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
@@ -1112,13 +1165,13 @@ def course_grader_updates(request, org, course, name, grader_index=None):
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
|
||||
|
||||
if real_method == 'GET':
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course',name]), grader_index)),
|
||||
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course',name]), grader_index)),
|
||||
mimetype="application/json")
|
||||
elif real_method == "DELETE":
|
||||
# ??? Shoudl this return anything? Perhaps success fail?
|
||||
# ??? Shoudl this return anything? Perhaps success fail?
|
||||
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course',name]), grader_index)
|
||||
return HttpResponse()
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
@@ -1135,7 +1188,7 @@ def asset_index(request, org, course, name):
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
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()
|
||||
@@ -1148,7 +1201,7 @@ def asset_index(request, org, course, name):
|
||||
})
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
|
||||
course_reference = StaticContent.compute_location(org, course, name)
|
||||
assets = contentstore().get_all_content_for_course(course_reference)
|
||||
|
||||
@@ -1162,15 +1215,15 @@ def asset_index(request, org, course, name):
|
||||
display_info = {}
|
||||
display_info['displayname'] = asset['displayname']
|
||||
display_info['uploadDate'] = get_date_display(asset['uploadDate'])
|
||||
|
||||
|
||||
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
|
||||
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
|
||||
|
||||
|
||||
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
|
||||
_thumbnail_location = asset.get('thumbnail_location', None)
|
||||
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
|
||||
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
|
||||
|
||||
|
||||
asset_display.append(display_info)
|
||||
|
||||
return render_to_response('asset_index.html', {
|
||||
@@ -1189,9 +1242,9 @@ def edge(request):
|
||||
@expect_json
|
||||
def create_new_course(request):
|
||||
template = Location(request.POST['template'])
|
||||
org = request.POST.get('org')
|
||||
number = request.POST.get('number')
|
||||
display_name = request.POST.get('display_name')
|
||||
org = request.POST.get('org')
|
||||
number = request.POST.get('number')
|
||||
display_name = request.POST.get('display_name')
|
||||
|
||||
try:
|
||||
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
|
||||
@@ -1237,13 +1290,13 @@ def initialize_course_tabs(course):
|
||||
# at least a list populated with the minimal times
|
||||
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
|
||||
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
|
||||
course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
|
||||
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
@@ -1337,7 +1390,7 @@ def generate_export_course(request, org, course, name):
|
||||
root_dir = path(mkdtemp())
|
||||
|
||||
# export out to a tempdir
|
||||
|
||||
|
||||
logging.debug('root = {0}'.format(root_dir))
|
||||
|
||||
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
|
||||
@@ -1349,7 +1402,7 @@ def generate_export_course(request, org, course, name):
|
||||
tf.close()
|
||||
|
||||
# remove temp dir
|
||||
shutil.rmtree(root_dir/name)
|
||||
shutil.rmtree(root_dir/name)
|
||||
|
||||
wrapper = FileWrapper(export_file)
|
||||
response = HttpResponse(wrapper, content_type='application/x-tgz')
|
||||
|
||||
@@ -15,7 +15,7 @@ class CMS.Views.TabsEdit extends Backbone.View
|
||||
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: (event, ui) => alert 'not yet implemented!'
|
||||
update: @tabMoved
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
@@ -24,6 +24,20 @@ class CMS.Views.TabsEdit extends Backbone.View
|
||||
items: '> .component'
|
||||
)
|
||||
|
||||
tabMoved: (event, ui) =>
|
||||
tabs = []
|
||||
@$('.component').each((idx, element) =>
|
||||
tabs.push($(element).data('id'))
|
||||
)
|
||||
$.ajax({
|
||||
type:'POST',
|
||||
url: '/reorder_static_tabs',
|
||||
data: JSON.stringify({
|
||||
tabs : tabs
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
})
|
||||
|
||||
addNewTab: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
|
||||
@@ -68,10 +68,12 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
save_videosource: function(newsource) {
|
||||
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
|
||||
// returns the videosource for the preview which iss the key whose speed is closest to 1
|
||||
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
|
||||
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null},
|
||||
{ error : CMS.ServerError});
|
||||
// TODO remove all whitespace w/in string
|
||||
else {
|
||||
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
|
||||
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource,
|
||||
{ error : CMS.ServerError});
|
||||
}
|
||||
|
||||
return this.videosourceSample();
|
||||
|
||||
@@ -99,10 +99,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
var targetModel = this.eventModel(event);
|
||||
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
|
||||
// push change to display, hide the editor, submit the change
|
||||
targetModel.save({}, {error : function(model, xhr) {
|
||||
// TODO use a standard component
|
||||
window.alert(xhr.responseText);
|
||||
}});
|
||||
targetModel.save({}, {error : CMS.ServerError});
|
||||
this.closeEditor(this);
|
||||
},
|
||||
|
||||
@@ -145,8 +142,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
this.modelDom(event).remove();
|
||||
var cacheThis = this;
|
||||
targetModel.destroy({success : function (model, response) {
|
||||
cacheThis.collection.fetch({success : function() {cacheThis.render();}});
|
||||
}
|
||||
cacheThis.collection.fetch({success : function() {cacheThis.render();},
|
||||
error : CMS.ServerError});
|
||||
},
|
||||
error : CMS.ServerError
|
||||
});
|
||||
},
|
||||
|
||||
@@ -225,7 +224,8 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
error : CMS.ServerError
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -267,7 +267,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
onSave: function(event) {
|
||||
this.model.set('data', this.$codeMirror.getValue());
|
||||
this.render();
|
||||
this.model.save();
|
||||
this.model.save({}, {error: CMS.ServerError});
|
||||
this.$form.hide();
|
||||
this.closeEditor(this);
|
||||
},
|
||||
|
||||
4
cms/static/js/views/server_error.js
Normal file
4
cms/static/js/views/server_error.js
Normal file
@@ -0,0 +1,4 @@
|
||||
CMS.ServerError = function(model, error) {
|
||||
// this handler is for the client:server communication not the validation errors which handleValidationError catches
|
||||
window.alert("Server Error: " + error.responseText);
|
||||
};
|
||||
@@ -55,7 +55,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
|
||||
var newVal = $(event.currentTarget).val();
|
||||
if (currentVal != newVal) {
|
||||
this.clearValidationErrors();
|
||||
this.model.save(field, newVal);
|
||||
this.model.save(field, newVal, { error : CMS.ServerError});
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
@@ -227,7 +227,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
time = 0;
|
||||
}
|
||||
var newVal = new Date(date.getTime() + time * 1000);
|
||||
if (cacheModel.get(fieldName) != newVal) cacheModel.save(fieldName, newVal);
|
||||
if (cacheModel.get(fieldName) != newVal) cacheModel.save(fieldName, newVal,
|
||||
{ error : CMS.ServerError});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -276,7 +277,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
},
|
||||
|
||||
removeSyllabus: function() {
|
||||
if (this.model.has('syllabus')) this.model.save({'syllabus': null});
|
||||
if (this.model.has('syllabus')) this.model.save({'syllabus': null},
|
||||
{ error : CMS.ServerError});
|
||||
},
|
||||
|
||||
assetSyllabus : function() {
|
||||
@@ -309,7 +311,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
mirror.save();
|
||||
cachethis.clearValidationErrors();
|
||||
var newVal = mirror.getValue();
|
||||
if (cachethis.model.get(field) != newVal) cachethis.model.save(field, newVal);
|
||||
if (cachethis.model.get(field) != newVal) cachethis.model.save(field, newVal,
|
||||
{ error : CMS.ServerError});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -404,7 +407,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
setGracePeriod : function(event) {
|
||||
event.data.clearValidationErrors();
|
||||
var newVal = event.data.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
|
||||
if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal);
|
||||
if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal,
|
||||
{ error : CMS.ServerError});
|
||||
},
|
||||
updateModel : function(event) {
|
||||
if (!this.selectorToField[event.currentTarget.id]) return;
|
||||
@@ -540,7 +544,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
|
||||
return object;
|
||||
},
|
||||
{}));
|
||||
{}),
|
||||
{ error : CMS.ServerError});
|
||||
},
|
||||
|
||||
addNewGrade: function(e) {
|
||||
@@ -671,7 +676,8 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
|
||||
}
|
||||
},
|
||||
deleteModel : function(e) {
|
||||
this.model.destroy();
|
||||
this.model.destroy(
|
||||
{ error : CMS.ServerError});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<script type="text/javascript" src="${static.url('js/models/course_info.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/module_info.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/course_info_edit.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
|
||||
|
||||
@@ -20,6 +20,7 @@ from contentstore import utils
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_settings.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
@@ -17,6 +17,7 @@ urlpatterns = ('',
|
||||
url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_draft'),
|
||||
url(r'^unpublish_unit$', 'contentstore.views.unpublish_unit', name='unpublish_unit'),
|
||||
url(r'^create_new_course', 'contentstore.views.create_new_course', name='create_new_course'),
|
||||
url(r'^reorder_static_tabs', 'contentstore.views.reorder_static_tabs', name='reorder_static_tabs'),
|
||||
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
|
||||
'contentstore.views.course_index', name='course_index'),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from optparse import make_option
|
||||
from json import dump
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
@@ -32,10 +33,9 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) < 1:
|
||||
raise CommandError("Missing single argument: output JSON file")
|
||||
|
||||
# get output location:
|
||||
outputfile = args[0]
|
||||
outputfile = datetime.utcnow().strftime("pearson-dump-%Y%m%d-%H%M%S.json")
|
||||
else:
|
||||
outputfile = args[0]
|
||||
|
||||
# construct the query object to dump:
|
||||
registrations = TestCenterRegistration.objects.all()
|
||||
@@ -65,6 +65,8 @@ class Command(BaseCommand):
|
||||
}
|
||||
if len(registration.upload_error_message) > 0:
|
||||
record['registration_error'] = registration.upload_error_message
|
||||
if len(registration.testcenter_user.upload_error_message) > 0:
|
||||
record['demographics_error'] = registration.testcenter_user.upload_error_message
|
||||
if registration.needs_uploading:
|
||||
record['needs_uploading'] = True
|
||||
|
||||
@@ -72,5 +74,5 @@ class Command(BaseCommand):
|
||||
|
||||
# dump output:
|
||||
with open(outputfile, 'w') as outfile:
|
||||
dump(output, outfile)
|
||||
dump(output, outfile, indent=2)
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ class CapaModule(XModule):
|
||||
'element_id': self.location.html_id(),
|
||||
'id': self.id,
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'progress': Progress.to_js_status_str(self.get_progress())
|
||||
})
|
||||
|
||||
def get_problem_html(self, encapsulate=True):
|
||||
@@ -355,7 +356,7 @@ class CapaModule(XModule):
|
||||
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
|
||||
|
||||
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
|
||||
return self.system.replace_urls(html, self.metadata['data_dir'], course_namespace=self.location)
|
||||
return self.system.replace_urls(html)
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
@@ -460,7 +461,7 @@ class CapaModule(XModule):
|
||||
new_answers = dict()
|
||||
for answer_id in answers:
|
||||
try:
|
||||
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'], course_namespace=self.location)}
|
||||
new_answer = {answer_id: self.system.replace_urls(answers[answer_id])}
|
||||
except TypeError:
|
||||
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
|
||||
new_answer = {answer_id: answers[answer_id]}
|
||||
@@ -668,18 +669,18 @@ class CapaDescriptor(RawDescriptor):
|
||||
# TODO (vshnayder): do problems have any other metadata? Do they
|
||||
# actually use type and points?
|
||||
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
|
||||
|
||||
|
||||
def get_context(self):
|
||||
_context = RawDescriptor.get_context(self)
|
||||
_context.update({'markdown': self.metadata.get('markdown', '')})
|
||||
return _context
|
||||
|
||||
|
||||
@property
|
||||
def editable_metadata_fields(self):
|
||||
"""Remove metadata from the editable fields since it has its own editor"""
|
||||
subset = super(CapaDescriptor,self).editable_metadata_fields
|
||||
if 'markdown' in subset:
|
||||
subset.remove('markdown')
|
||||
subset.remove('markdown')
|
||||
return subset
|
||||
|
||||
|
||||
|
||||
@@ -17,4 +17,23 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
|
||||
# export the static assets
|
||||
contentstore.export_all_for_course(course_location, root_dir + '/' + course_dir + '/static/')
|
||||
|
||||
# export the static tabs
|
||||
export_extra_content(export_fs, modulestore, course_location, 'static_tab', 'tabs', '.html')
|
||||
|
||||
# export the custom tags
|
||||
export_extra_content(export_fs, modulestore, course_location, 'custom_tag_template', 'custom_tags')
|
||||
|
||||
|
||||
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix = ''):
|
||||
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)
|
||||
items = modulestore.get_items(query_loc)
|
||||
|
||||
if len(items) > 0:
|
||||
item_dir = export_fs.makeopendir(dirname)
|
||||
for item in items:
|
||||
with item_dir.open(item.location.name + file_suffix, 'w') as item_file:
|
||||
item_file.write(item.definition['data'].encode('utf8'))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "static_tab", "url_slug": "syllabus", "name": "Syllabus"},
|
||||
{"type": "static_tab", "url_slug": "resources", "name": "Resources"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}
|
||||
|
||||
1
common/test/data/full/tabs/resources.html
Normal file
1
common/test/data/full/tabs/resources.html
Normal file
@@ -0,0 +1 @@
|
||||
This is another sample tab
|
||||
@@ -83,13 +83,12 @@ def get_opt_course_with_access(user, course_id, action):
|
||||
return None
|
||||
return get_course_with_access(user, course_id, action)
|
||||
|
||||
|
||||
|
||||
def course_image_url(course):
|
||||
"""Try to look up the image url for the course. If it's not found,
|
||||
log an error and return the dead link"""
|
||||
if isinstance(modulestore(), XMLModuleStore):
|
||||
path = course.metadata['data_dir'] + "/images/course_image.jpg"
|
||||
return try_staticfiles_lookup(path)
|
||||
return '/static/' + course.metadata['data_dir'] + "/images/course_image.jpg"
|
||||
else:
|
||||
loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg')
|
||||
path = StaticContent.get_url_path_from_location(loc)
|
||||
|
||||
@@ -3,6 +3,8 @@ import logging
|
||||
import pyparsing
|
||||
import sys
|
||||
|
||||
from functools import partial
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -244,7 +246,11 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
|
||||
# TODO (cpennington): This should be removed when all html from
|
||||
# a module is coming through get_html and is therefore covered
|
||||
# by the replace_static_urls code below
|
||||
replace_urls=replace_urls,
|
||||
replace_urls=partial(
|
||||
replace_urls,
|
||||
staticfiles_prefix='/static/' + descriptor.metadata.get('data_dir', ''),
|
||||
course_namespace=descriptor.location._replace(category=None, name=None),
|
||||
),
|
||||
node_path=settings.NODE_PATH,
|
||||
anonymous_student_id=unique_id_for_user(user),
|
||||
course_id=course_id,
|
||||
@@ -280,7 +286,7 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
|
||||
|
||||
module.get_html = replace_static_urls(
|
||||
_get_html,
|
||||
module.metadata['data_dir'] if 'data_dir' in module.metadata else '',
|
||||
'/static/' + module.metadata.get('data_dir', ''),
|
||||
course_namespace = module.location._replace(category=None, name=None))
|
||||
|
||||
# Allow URLs of the form '/course/' refer to the root of multicourse directory
|
||||
|
||||
44
lms/djangoapps/courseware/tests/factories.py
Normal file
44
lms/djangoapps/courseware/tests/factories.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import factory
|
||||
from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed)
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
class UserProfileFactory(factory.Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Studio'
|
||||
courseware = 'course.xml'
|
||||
|
||||
class RegistrationFactory(factory.Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid.uuid4().hex
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Tester'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime.now()
|
||||
date_joined = datetime.now()
|
||||
|
||||
class GroupFactory(factory.Factory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
name = 'test_group'
|
||||
|
||||
class CourseEnrollmentAllowedFactory(factory.Factory):
|
||||
FACTORY_FOR = CourseEnrollmentAllowed
|
||||
|
||||
email = 'test@edx.org'
|
||||
course_id = 'edX/test/2012_Fall'
|
||||
109
lms/djangoapps/courseware/tests/test_access.py
Normal file
109
lms/djangoapps/courseware/tests/test_access.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import unittest
|
||||
import time
|
||||
from mock import Mock
|
||||
from django.test import TestCase
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from factories import CourseEnrollmentAllowedFactory
|
||||
import courseware.access as access
|
||||
|
||||
class AccessTestCase(TestCase):
|
||||
def test__has_global_staff_access(self):
|
||||
u = Mock(is_staff=False)
|
||||
self.assertFalse(access._has_global_staff_access(u))
|
||||
|
||||
u = Mock(is_staff=True)
|
||||
self.assertTrue(access._has_global_staff_access(u))
|
||||
|
||||
def test__has_access_to_location(self):
|
||||
location = Location('i4x://edX/toy/course/2012_Fall')
|
||||
|
||||
self.assertFalse(access._has_access_to_location(None, location,
|
||||
'staff', None))
|
||||
u = Mock()
|
||||
u.is_authenticated.return_value = False
|
||||
self.assertFalse(access._has_access_to_location(u, location,
|
||||
'staff', None))
|
||||
u = Mock(is_staff=True)
|
||||
self.assertTrue(access._has_access_to_location(u, location,
|
||||
'instructor', None))
|
||||
# A user has staff access if they are in the staff group
|
||||
u = Mock(is_staff=False)
|
||||
g = Mock()
|
||||
g.name = 'staff_edX/toy/2012_Fall'
|
||||
u.groups.all.return_value = [g]
|
||||
self.assertTrue(access._has_access_to_location(u, location,
|
||||
'staff', None))
|
||||
# A user has staff access if they are in the instructor group
|
||||
g.name = 'instructor_edX/toy/2012_Fall'
|
||||
self.assertTrue(access._has_access_to_location(u, location,
|
||||
'staff', None))
|
||||
|
||||
# A user has instructor access if they are in the instructor group
|
||||
g.name = 'instructor_edX/toy/2012_Fall'
|
||||
self.assertTrue(access._has_access_to_location(u, location,
|
||||
'instructor', None))
|
||||
|
||||
# A user does not have staff access if they are
|
||||
# not in either the staff or the the instructor group
|
||||
g.name = 'student_only'
|
||||
self.assertFalse(access._has_access_to_location(u, location,
|
||||
'staff', None))
|
||||
|
||||
# A user does not have instructor access if they are
|
||||
# not in the instructor group
|
||||
g.name = 'student_only'
|
||||
self.assertFalse(access._has_access_to_location(u, location,
|
||||
'instructor', None))
|
||||
|
||||
def test__has_access_string(self):
|
||||
u = Mock(is_staff=True)
|
||||
self.assertFalse(access._has_access_string(u, 'not_global', 'staff', None))
|
||||
|
||||
u._has_global_staff_access.return_value = True
|
||||
self.assertTrue(access._has_access_string(u, 'global', 'staff', None))
|
||||
|
||||
self.assertRaises(ValueError, access._has_access_string, u, 'global', 'not_staff', None)
|
||||
|
||||
def test__has_access_descriptor(self):
|
||||
# TODO: override DISABLE_START_DATES and test the start date branch of the method
|
||||
u = Mock()
|
||||
d = Mock()
|
||||
d.start = time.gmtime(time.time() - 86400) # make sure the start time is in the past
|
||||
|
||||
# Always returns true because DISABLE_START_DATES is set in test.py
|
||||
self.assertTrue(access._has_access_descriptor(u, d, 'load'))
|
||||
self.assertRaises(ValueError, access._has_access_descriptor, u, d, 'not_load_or_staff')
|
||||
|
||||
def test__has_access_course_desc_can_enroll(self):
|
||||
u = Mock()
|
||||
yesterday = time.gmtime(time.time() - 86400)
|
||||
tomorrow = time.gmtime(time.time() + 86400)
|
||||
c = Mock(enrollment_start=yesterday, enrollment_end=tomorrow)
|
||||
c.metadata.get = 'is_public'
|
||||
|
||||
# User can enroll if it is between the start and end dates
|
||||
self.assertTrue(access._has_access_course_desc(u, c, 'enroll'))
|
||||
|
||||
# User can enroll if authenticated and specifically allowed for that course
|
||||
# even outside the open enrollment period
|
||||
u = Mock(email='test@edx.org', is_staff=False)
|
||||
u.is_authenticated.return_value = True
|
||||
|
||||
c = Mock(enrollment_start=tomorrow, enrollment_end=tomorrow, id='edX/test/2012_Fall')
|
||||
c.metadata.get = 'is_public'
|
||||
|
||||
allowed = CourseEnrollmentAllowedFactory(email=u.email, course_id=c.id)
|
||||
|
||||
self.assertTrue(access._has_access_course_desc(u, c, 'enroll'))
|
||||
|
||||
# Staff can always enroll even outside the open enrollment period
|
||||
u = Mock(email='test@edx.org', is_staff=True)
|
||||
u.is_authenticated.return_value = True
|
||||
|
||||
c = Mock(enrollment_start=tomorrow, enrollment_end=tomorrow, id='edX/test/Whenever')
|
||||
c.metadata.get = 'is_public'
|
||||
self.assertTrue(access._has_access_course_desc(u, c, 'enroll'))
|
||||
|
||||
# TODO:
|
||||
# Non-staff cannot enroll outside the open enrollment period if not specifically allowed
|
||||
@@ -64,6 +64,21 @@ def mongo_store_config(data_dir):
|
||||
}
|
||||
}
|
||||
|
||||
def draft_mongo_store_config(data_dir):
|
||||
return {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
|
||||
'OPTIONS': {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def xml_store_config(data_dir):
|
||||
return {
|
||||
'default': {
|
||||
@@ -78,6 +93,7 @@ def xml_store_config(data_dir):
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR)
|
||||
|
||||
class ActivateLoginTestCase(TestCase):
|
||||
'''Check that we can activate and log in'''
|
||||
@@ -423,6 +439,16 @@ class TestNavigation(PageLoader):
|
||||
kwargs={'course_id': self.toy.id, 'chapter': 'secret:magic'}))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE)
|
||||
class TestDraftModuleStore(TestCase):
|
||||
def test_get_items_with_course_items(self):
|
||||
store = modulestore()
|
||||
# fix was to allow get_items() to take the course_id parameter
|
||||
store.get_items(Location(None, None, 'vertical', None, None), course_id='abc', depth=0)
|
||||
# test success is just getting through the above statement. The bug was that 'course_id' argument was
|
||||
# not allowed to be passed in (i.e. was throwing exception)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestViewAuth(PageLoader):
|
||||
"""Check that view authentication works properly"""
|
||||
|
||||
@@ -233,10 +233,13 @@ def index(request, course_id, chapter=None, section=None,
|
||||
# Specifically asked-for section doesn't exist
|
||||
raise Http404
|
||||
|
||||
# Load all descendents of the section, because we're going to display it's
|
||||
# Load all descendants of the section, because we're going to display its
|
||||
# html, which in general will need all of its children
|
||||
section_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
course.id, request.user, section_descriptor, depth=None)
|
||||
|
||||
section_module = get_module(request.user, request, section_descriptor.location,
|
||||
student_module_cache, course.id, position=position, depth=None)
|
||||
section_module_cache, course.id, position=position, depth=None)
|
||||
if section_module is None:
|
||||
# User may be trying to be clever and access something
|
||||
# they don't have access to.
|
||||
|
||||
@@ -266,24 +266,6 @@ STATICFILES_DIRS = [
|
||||
COMMON_ROOT / "static",
|
||||
PROJECT_ROOT / "static",
|
||||
]
|
||||
if os.path.isdir(DATA_DIR):
|
||||
# Add the full course repo if there is no static directory
|
||||
STATICFILES_DIRS += [
|
||||
# TODO (cpennington): When courses are stored in a database, this
|
||||
# should no longer be added to STATICFILES
|
||||
(course_dir, DATA_DIR / course_dir)
|
||||
for course_dir in os.listdir(DATA_DIR)
|
||||
if (os.path.isdir(DATA_DIR / course_dir) and
|
||||
not os.path.isdir(DATA_DIR / course_dir / 'static'))
|
||||
]
|
||||
# Otherwise, add only the static directory from the course dir
|
||||
STATICFILES_DIRS += [
|
||||
# TODO (cpennington): When courses are stored in a database, this
|
||||
# should no longer be added to STATICFILES
|
||||
(course_dir, DATA_DIR / course_dir / 'static')
|
||||
for course_dir in os.listdir(DATA_DIR)
|
||||
if (os.path.isdir(DATA_DIR / course_dir / 'static'))
|
||||
]
|
||||
|
||||
# Locale/Internationalization
|
||||
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
@@ -468,7 +450,7 @@ PIPELINE_JS = {
|
||||
'source_filenames': sorted(
|
||||
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') +
|
||||
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) -
|
||||
set(courseware_js + discussion_js + staff_grading_js + peer_grading_js)
|
||||
set(courseware_js + discussion_js + staff_grading_js + peer_grading_js)
|
||||
) + [
|
||||
'js/form.ext.js',
|
||||
'js/my_courses_dropdown.js',
|
||||
|
||||
@@ -106,6 +106,27 @@ VIRTUAL_UNIVERSITIES = []
|
||||
|
||||
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
|
||||
|
||||
############################## Course static files ##########################
|
||||
if os.path.isdir(DATA_DIR):
|
||||
# Add the full course repo if there is no static directory
|
||||
STATICFILES_DIRS += [
|
||||
# TODO (cpennington): When courses are stored in a database, this
|
||||
# should no longer be added to STATICFILES
|
||||
(course_dir, DATA_DIR / course_dir)
|
||||
for course_dir in os.listdir(DATA_DIR)
|
||||
if (os.path.isdir(DATA_DIR / course_dir) and
|
||||
not os.path.isdir(DATA_DIR / course_dir / 'static'))
|
||||
]
|
||||
# Otherwise, add only the static directory from the course dir
|
||||
STATICFILES_DIRS += [
|
||||
# TODO (cpennington): When courses are stored in a database, this
|
||||
# should no longer be added to STATICFILES
|
||||
(course_dir, DATA_DIR / course_dir / 'static')
|
||||
for course_dir in os.listdir(DATA_DIR)
|
||||
if (os.path.isdir(DATA_DIR / course_dir / 'static'))
|
||||
]
|
||||
|
||||
|
||||
################################# mitx revision string #####################
|
||||
|
||||
MITX_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip()
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
##<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/>
|
||||
<title>EdX Blog</title>
|
||||
<updated>2013-01-21T14:00:12-07:00</updated>
|
||||
<entry>
|
||||
<id>tag:www.edx.org,2012:Post/12</id>
|
||||
<published>2013-01-29T10:00:00-07:00</published>
|
||||
<updated>2013-01-29T10:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/bostonx-announcement')}"/>
|
||||
<title>City of Boston and edX partner to establish BostonX to improve educational access for residents</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/edx-logo_240x180.png')}" />
|
||||
<p></p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>tag:www.edx.org,2012:Post/11</id>
|
||||
<published>2013-01-22T10:00:00-07:00</published>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<section id="problem_${element_id}" class="problems-wrapper" data-problem-id="${id}" data-url="${ajax_url}"></section>
|
||||
<section id="problem_${element_id}" class="problems-wrapper" data-problem-id="${id}" data-url="${ajax_url}" progress="${progress}"></section>
|
||||
|
||||
@@ -39,7 +39,14 @@
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>Will certificates be awarded?</h3>
|
||||
<p>Yes. Online learners who demonstrate mastery of subjects can earn a certificate of mastery. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, <em>MITx</em> or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.</p>
|
||||
<p>Yes. Online learners who demonstrate mastery of subjects can earn a certificate
|
||||
of mastery. Certificates will be issued at the discretion of edX and the underlying
|
||||
X University that offered the course under the name of the underlying "X
|
||||
University" from where the course originated, i.e. HarvardX, MITx or BerkeleyX.
|
||||
For the courses in Fall 2012, those certificates will be free. There is a plan to
|
||||
charge a modest fee for certificates in the future. Note: At this time, edX is
|
||||
holding certificates for learners connected with Cuba, Iran, Syria and Sudan
|
||||
pending confirmation that the issuance is in compliance with U.S. embargoes.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>What will the scope of the online courses be? How many? Which faculty?</h3>
|
||||
|
||||
@@ -180,8 +180,17 @@
|
||||
|
||||
<article class="response">
|
||||
<h3 class="question">Will I get a certificate for taking an edX course?</h3>
|
||||
<div class="answer" id="certificates_and_credits_faq_answer_0">
|
||||
<p>Online learners who receive a passing grade for a course will receive a certificate of mastery from edX and the underlying X University that offered the course. For example, a certificate of mastery for MITx’s 6.002x Circuits & Electronics will come from edX and MITx.</p>
|
||||
<div class ="answer" id="certificates_and_credits_faq_answer_0">
|
||||
<p>Online learners who receive a passing grade for a course will receive a certificate
|
||||
of mastery at the discretion of edX and the underlying X University that offered
|
||||
the course. For example, a certificate of mastery for MITx’s 6.002x Circuits &
|
||||
Electronics will come from edX and MITx.</p>
|
||||
<p>If you passed the course, your certificate of mastery will be delivered online
|
||||
through edx.org. So be sure to check your email in the weeks following the final
|
||||
grading – you will be able to download and print your certificate. Note: At this
|
||||
time, edX is holding certificates for learners connected with Cuba, Iran, Syria
|
||||
and Sudan pending confirmation that the issuance is in compliance with U.S.
|
||||
embargoes.</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="response">
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../../main.html" />
|
||||
|
||||
<%namespace name='static' file='../../static_content.html'/>
|
||||
|
||||
<%block name="title"><title>City of Boston and edX partner to establish BostonX to improve educational access for residents</title></%block>
|
||||
<div id="fb-root"></div>
|
||||
<script>(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));</script>
|
||||
|
||||
<section class="pressrelease">
|
||||
<section class="container">
|
||||
<h1>City of Boston and edX partner to establish BostonX to improve educational access for residents</h1>
|
||||
<hr class="horizontal-divider">
|
||||
<article>
|
||||
<h2>Pilot project offers online courses, educational support and jobs training through Boston community centers</h2>
|
||||
|
||||
<p><strong>CAMBRIDGE, MA – January 29, 2013 –</strong>
|
||||
<a href="http://www.edx.org">EdX</a>, the not-for-profit online learning initiative founded by Harvard University and the Massachusetts Institute of Technology (MIT), announced today a pilot project with the City of Boston, Harvard and MIT to make online courses available through internet-connected Boston neighborhood community centers, high schools and libraries. A first-of-its-kind project, BostonX brings together innovators from the country’s center of higher education to offer Boston residents access to courses, internships, job training and placement services, and locations for edX students to gather, socialize and deepen learning.</p>
|
||||
|
||||
<p>“We must connect adults and youth in our neighborhoods with the opportunities of the knowledge economy,” said Mayor Tom Menino. “BostonX will help update our neighbors’ skills and our community centers. As a first step, I’m pleased to announce a pilot with Harvard, MIT and edX, their online learning initiative, which will bring free courses and training to our community centers.”</p>
|
||||
|
||||
<p>BostonX builds on edX’s mission of expanding access to education and delivering high-quality courses on its cutting-edge platform using innovative tools and educational techniques. The City of Boston will provide BostonX sites at community centers with computer access and basic computer training, support for internships, career counseling, and job transitioning. Harvard, MIT and edX will work with the city to provide courses selected to eliminate skills gaps, in-person lessons from affiliated instructors, training in online learning best practices and certificates of mastery for those who successfully complete the courses.</p>
|
||||
|
||||
<p>“EdX’s innovative content, learning methodologies and game-like laboratories and teaching methods are transforming education, from 16-year-old students in Bangladesh, to community college students at Bunker Hill and MassBay, and now learners across Boston,” said Anant Agarwal, President of edX. “We’re thrilled to be able to partner with Mayor Menino and the City of Boston to provide this first-ever experience and hope that this idea will spread and create a number of CityX’s around the world, including Cambridge, Massachusetts where edX was founded.”</p>
|
||||
|
||||
<p>This new pilot with the City of Boston follows another edX project with two Boston-area community colleges. This month, Bunker Hill and MassBay Community Colleges began offering an adapted version of the <a href="https://www.edx.org/courses/MITx/6.00x/2013_Spring/about">MITx 6.00x Introduction to Computer Science and Programming</a> course at their respective campuses. The BostonX initiative goes one step further by allowing, encouraging and supporting residents of all ages, regardless of social status or neighborhood, to participate in life changing educational opportunities.</p>
|
||||
|
||||
<h2>About edX</h2>
|
||||
|
||||
<p><a href="https://www.edx.org/">EdX</a> is a not-for-profit enterprise of its founding partners <a href="http://www.harvard.edu">Harvard University</a> and the <a href="http://www.mit.edu">Massachusetts Institute of Technology</a> focused on transforming online and on-campus learning through groundbreaking methodologies, game-like experiences and cutting-edge research. EdX provides inspirational and transformative knowledge to students of all ages, social status, and income who form worldwide communities of learners. EdX uses its open source technology to transcend physical and social borders. We’re focused on people, not profit. EdX is based in Cambridge, Massachusetts in the USA.</p>
|
||||
|
||||
|
||||
<section class="contact">
|
||||
<p><strong>Contact:</strong></p>
|
||||
<p>Brad Baker, Weber Shandwick for edX</p>
|
||||
<p>BBaker@webershandwick.com</p>
|
||||
<p>(617) 520-7043</p>
|
||||
</section>
|
||||
|
||||
<section class="footer">
|
||||
<hr class="horizontal-divider">
|
||||
<div class="logo"></div><h3 class="date">01 - 29 - 2013</h3>
|
||||
<div class="social-sharing">
|
||||
<hr class="horizontal-divider">
|
||||
<p>Share with friends and family:</p>
|
||||
<a href="http://twitter.com/intent/tweet?text=:City+of+Boston+and+edX+partner+to+establish+BostonX+to+improve+educational+access+for+residents+http://www.edx.org/press/bostonx-announcement" class="share">
|
||||
<img src="${static.url('images/social/twitter-sharing.png')}">
|
||||
</a>
|
||||
</a>
|
||||
<a href="mailto:?subject=City%20of%20Boston%20and%20edX%20partner%20to%20establish%20BostonX%20to%20improve%20educational%20access%20for%20residents…http://edx.org/press/bostonx-announcement" class="share">
|
||||
<img src="${static.url('images/social/email-sharing.png')}">
|
||||
</a>
|
||||
<div class="fb-like" data-href="http://edx.org/press/bostonx-announcement" data-send="true" data-width="450" data-show-faces="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
13
lms/urls.py
13
lms/urls.py
@@ -117,12 +117,19 @@ urlpatterns = ('',
|
||||
url(r'^press/georgetown-joins-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Georgetown_joins_edX.html'}, name="press/georgetown-joins-edx"),
|
||||
url(r'^press/spring-courses$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Spring_2013_course_announcements.html'}, name="press/spring-courses"),
|
||||
{'template': 'press_releases/Spring_2013_course_announcements.html'},
|
||||
name="press/spring-courses"),
|
||||
url(r'^press/lewin-course-announcement$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Lewin_course_announcement.html'}, name="press/lewin-course-announcement"),
|
||||
{'template': 'press_releases/Lewin_course_announcement.html'},
|
||||
name="press/lewin-course-announcement"),
|
||||
url(r'^press/bostonx-announcement$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/bostonx_announcement.html'},
|
||||
name="press/bostonx-announcement"),
|
||||
|
||||
|
||||
# Should this always update to point to the latest press release?
|
||||
(r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/lewin-course-announcement'}),
|
||||
(r'^pressrelease$', 'django.views.generic.simple.redirect_to',
|
||||
{'url': '/press/bostonx-announcement'}),
|
||||
|
||||
|
||||
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
|
||||
|
||||
Reference in New Issue
Block a user