Commit 46cd9b0a by John Eskew

Add bulk_operations wrapper around all course-query-intensive Studio views.

parent 2f060733
...@@ -141,74 +141,75 @@ def container_handler(request, usage_key_string): ...@@ -141,74 +141,75 @@ def container_handler(request, usage_key_string):
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
usage_key = UsageKey.from_string(usage_key_string) usage_key = UsageKey.from_string(usage_key_string)
try: with modulestore().bulk_operations(usage_key.course_key):
course, xblock, lms_link = _get_item_in_course(request, usage_key) try:
except ItemNotFoundError: course, xblock, lms_link = _get_item_in_course(request, usage_key)
return HttpResponseBadRequest() except ItemNotFoundError:
return HttpResponseBadRequest()
component_templates = get_component_templates(course)
ancestor_xblocks = [] component_templates = get_component_templates(course)
parent = get_parent_xblock(xblock) ancestor_xblocks = []
action = request.REQUEST.get('action', 'view') parent = get_parent_xblock(xblock)
action = request.REQUEST.get('action', 'view')
is_unit_page = is_unit(xblock)
unit = xblock if is_unit_page else None is_unit_page = is_unit(xblock)
unit = xblock if is_unit_page else None
while parent and parent.category != 'course':
if unit is None and is_unit(parent): while parent and parent.category != 'course':
unit = parent if unit is None and is_unit(parent):
ancestor_xblocks.append(parent) unit = parent
parent = get_parent_xblock(parent) ancestor_xblocks.append(parent)
ancestor_xblocks.reverse() parent = get_parent_xblock(parent)
ancestor_xblocks.reverse()
assert unit is not None, "Could not determine unit page"
subsection = get_parent_xblock(unit) assert unit is not None, "Could not determine unit page"
assert subsection is not None, "Could not determine parent subsection from unit " + unicode(unit.location) subsection = get_parent_xblock(unit)
section = get_parent_xblock(subsection) assert subsection is not None, "Could not determine parent subsection from unit " + unicode(unit.location)
assert section is not None, "Could not determine ancestor section from unit " + unicode(unit.location) section = get_parent_xblock(subsection)
assert section is not None, "Could not determine ancestor section from unit " + unicode(unit.location)
# Fetch the XBlock info for use by the container page. Note that it includes information
# about the block's ancestors and siblings for use by the Unit Outline. # Fetch the XBlock info for use by the container page. Note that it includes information
xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page) # about the block's ancestors and siblings for use by the Unit Outline.
xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page)
# Create the link for preview.
preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE') # Create the link for preview.
# need to figure out where this item is in the list of children as the preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')
# preview will need this # need to figure out where this item is in the list of children as the
index = 1 # preview will need this
for child in subsection.get_children(): index = 1
if child.location == unit.location: for child in subsection.get_children():
break if child.location == unit.location:
index += 1 break
preview_lms_link = ( index += 1
u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}' preview_lms_link = (
).format( u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'
preview_lms_base=preview_lms_base, ).format(
lms_base=settings.LMS_BASE, preview_lms_base=preview_lms_base,
org=course.location.org, lms_base=settings.LMS_BASE,
course=course.location.course, org=course.location.org,
course_name=course.location.name, course=course.location.course,
section=section.location.name, course_name=course.location.name,
subsection=subsection.location.name, section=section.location.name,
index=index subsection=subsection.location.name,
) index=index
)
return render_to_response('container.html', {
'context_course': course, # Needed only for display of menus at top of page. return render_to_response('container.html', {
'action': action, 'context_course': course, # Needed only for display of menus at top of page.
'xblock': xblock, 'action': action,
'xblock_locator': xblock.location, 'xblock': xblock,
'unit': unit, 'xblock_locator': xblock.location,
'is_unit_page': is_unit_page, 'unit': unit,
'subsection': subsection, 'is_unit_page': is_unit_page,
'section': section, 'subsection': subsection,
'new_unit_category': 'vertical', 'section': section,
'ancestor_xblocks': ancestor_xblocks, 'new_unit_category': 'vertical',
'component_templates': json.dumps(component_templates), 'ancestor_xblocks': ancestor_xblocks,
'xblock_info': xblock_info, 'component_templates': json.dumps(component_templates),
'draft_preview_link': preview_lms_link, 'xblock_info': xblock_info,
'published_preview_link': lms_link, 'draft_preview_link': preview_lms_link,
}) 'published_preview_link': lms_link,
})
else: else:
return HttpResponseBadRequest("Only supports HTML requests") return HttpResponseBadRequest("Only supports HTML requests")
......
...@@ -212,8 +212,10 @@ def course_handler(request, course_key_string=None): ...@@ -212,8 +212,10 @@ def course_handler(request, course_key_string=None):
response_format = request.REQUEST.get('format', 'html') response_format = request.REQUEST.get('format', 'html')
if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
if request.method == 'GET': if request.method == 'GET':
course_module = _get_course_module(CourseKey.from_string(course_key_string), request.user, depth=None) course_key = CourseKey.from_string(course_key_string)
return JsonResponse(_course_outline_json(request, course_module)) with modulestore().bulk_operations(course_key):
course_module = _get_course_module(course_key, request.user, depth=None)
return JsonResponse(_course_outline_json(request, course_module))
elif request.method == 'POST': # not sure if this is only post. If one will have ids, it goes after access elif request.method == 'POST': # not sure if this is only post. If one will have ids, it goes after access
return _create_or_rerun_course(request) return _create_or_rerun_course(request)
elif not has_course_access(request.user, CourseKey.from_string(course_key_string)): elif not has_course_access(request.user, CourseKey.from_string(course_key_string)):
...@@ -248,15 +250,16 @@ def course_rerun_handler(request, course_key_string): ...@@ -248,15 +250,16 @@ def course_rerun_handler(request, course_key_string):
if not GlobalStaff().has_user(request.user): if not GlobalStaff().has_user(request.user):
raise PermissionDenied() raise PermissionDenied()
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course_module = _get_course_module(course_key, request.user, depth=3) with modulestore().bulk_operations(course_key):
if request.method == 'GET': course_module = _get_course_module(course_key, request.user, depth=3)
return render_to_response('course-create-rerun.html', { if request.method == 'GET':
'source_course_key': course_key, return render_to_response('course-create-rerun.html', {
'display_name': course_module.display_name, 'source_course_key': course_key,
'user': request.user, 'display_name': course_module.display_name,
'course_creator_status': _get_course_creator_status(request.user), 'user': request.user,
'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False) 'course_creator_status': _get_course_creator_status(request.user),
}) 'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False)
})
def _course_outline_json(request, course_module): def _course_outline_json(request, course_module):
""" """
...@@ -644,20 +647,20 @@ def course_info_handler(request, course_key_string): ...@@ -644,20 +647,20 @@ def course_info_handler(request, course_key_string):
html: return html for editing the course info handouts and updates. html: return html for editing the course info handouts and updates.
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course_module = _get_course_module(course_key, request.user) with modulestore().bulk_operations(course_key):
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): course_module = _get_course_module(course_key, request.user)
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
return render_to_response( return render_to_response(
'course_info.html', 'course_info.html',
{ {
'context_course': course_module, 'context_course': course_module,
'updates_url': reverse_course_url('course_info_update_handler', course_key), 'updates_url': reverse_course_url('course_info_update_handler', course_key),
'handouts_locator': course_key.make_usage_key('course_info', 'handouts'), 'handouts_locator': course_key.make_usage_key('course_info', 'handouts'),
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_module.id) 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_module.id)
} }
) )
else: else:
return HttpResponseBadRequest("Only supports html requests") return HttpResponseBadRequest("Only supports html requests")
# pylint: disable=unused-argument # pylint: disable=unused-argument
...@@ -727,42 +730,43 @@ def settings_handler(request, course_key_string): ...@@ -727,42 +730,43 @@ def settings_handler(request, course_key_string):
json: update the Course and About xblocks through the CourseDetails model json: update the Course and About xblocks through the CourseDetails model
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course_module = _get_course_module(course_key, request.user) with modulestore().bulk_operations(course_key):
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': course_module = _get_course_module(course_key, request.user)
upload_asset_url = reverse_course_url('assets_handler', course_key) if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
upload_asset_url = reverse_course_url('assets_handler', course_key)
# see if the ORG of this course can be attributed to a 'Microsite'. In that case, the
# course about page should be editable in Studio # see if the ORG of this course can be attributed to a 'Microsite'. In that case, the
about_page_editable = not microsite.get_value_for_org( # course about page should be editable in Studio
course_module.location.org, about_page_editable = not microsite.get_value_for_org(
'ENABLE_MKTG_SITE', course_module.location.org,
settings.FEATURES.get('ENABLE_MKTG_SITE', False) 'ENABLE_MKTG_SITE',
) settings.FEATURES.get('ENABLE_MKTG_SITE', False)
)
short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True) short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True)
return render_to_response('settings.html', { return render_to_response('settings.html', {
'context_course': course_module, 'context_course': course_module,
'course_locator': course_key, 'course_locator': course_key,
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key), 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key),
'course_image_url': utils.course_image_url(course_module), 'course_image_url': utils.course_image_url(course_module),
'details_url': reverse_course_url('settings_handler', course_key), 'details_url': reverse_course_url('settings_handler', course_key),
'about_page_editable': about_page_editable, 'about_page_editable': about_page_editable,
'short_description_editable': short_description_editable, 'short_description_editable': short_description_editable,
'upload_asset_url': upload_asset_url 'upload_asset_url': upload_asset_url
}) })
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET': if request.method == 'GET':
return JsonResponse( return JsonResponse(
CourseDetails.fetch(course_key), CourseDetails.fetch(course_key),
# encoder serializes dates, old locations, and instances # encoder serializes dates, old locations, and instances
encoder=CourseSettingsEncoder encoder=CourseSettingsEncoder
) )
else: # post or put, doesn't matter. else: # post or put, doesn't matter.
return JsonResponse( return JsonResponse(
CourseDetails.update_from_json(course_key, request.json, request.user), CourseDetails.update_from_json(course_key, request.json, request.user),
encoder=CourseSettingsEncoder encoder=CourseSettingsEncoder
) )
@login_required @login_required
...@@ -781,41 +785,42 @@ def grading_handler(request, course_key_string, grader_index=None): ...@@ -781,41 +785,42 @@ def grading_handler(request, course_key_string, grader_index=None):
json w/ grader_index: create or update the specific grader (create if index out of range) json w/ grader_index: create or update the specific grader (create if index out of range)
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course_module = _get_course_module(course_key, request.user) with modulestore().bulk_operations(course_key):
course_module = _get_course_module(course_key, request.user)
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
course_details = CourseGradingModel.fetch(course_key) course_details = CourseGradingModel.fetch(course_key)
return render_to_response('settings_graders.html', { return render_to_response('settings_graders.html', {
'context_course': course_module, 'context_course': course_module,
'course_locator': course_key, 'course_locator': course_key,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder), 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder),
'grading_url': reverse_course_url('grading_handler', course_key), 'grading_url': reverse_course_url('grading_handler', course_key),
}) })
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET': if request.method == 'GET':
if grader_index is None: if grader_index is None:
return JsonResponse( return JsonResponse(
CourseGradingModel.fetch(course_key), CourseGradingModel.fetch(course_key),
# encoder serializes dates, old locations, and instances # encoder serializes dates, old locations, and instances
encoder=CourseSettingsEncoder encoder=CourseSettingsEncoder
) )
else: else:
return JsonResponse(CourseGradingModel.fetch_grader(course_key, grader_index)) return JsonResponse(CourseGradingModel.fetch_grader(course_key, grader_index))
elif request.method in ('POST', 'PUT'): # post or put, doesn't matter. elif request.method in ('POST', 'PUT'): # post or put, doesn't matter.
# None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader # None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader
if grader_index is None: if grader_index is None:
return JsonResponse( return JsonResponse(
CourseGradingModel.update_from_json(course_key, request.json, request.user), CourseGradingModel.update_from_json(course_key, request.json, request.user),
encoder=CourseSettingsEncoder encoder=CourseSettingsEncoder
) )
else: else:
return JsonResponse( return JsonResponse(
CourseGradingModel.update_grader_from_json(course_key, request.json, request.user) CourseGradingModel.update_grader_from_json(course_key, request.json, request.user)
) )
elif request.method == "DELETE" and grader_index is not None: elif request.method == "DELETE" and grader_index is not None:
CourseGradingModel.delete_grader(course_key, grader_index, request.user) CourseGradingModel.delete_grader(course_key, grader_index, request.user)
return JsonResponse() return JsonResponse()
# pylint: disable=invalid-name # pylint: disable=invalid-name
...@@ -892,41 +897,42 @@ def advanced_settings_handler(request, course_key_string): ...@@ -892,41 +897,42 @@ def advanced_settings_handler(request, course_key_string):
metadata dicts. metadata dicts.
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course_module = _get_course_module(course_key, request.user) with modulestore().bulk_operations(course_key):
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': course_module = _get_course_module(course_key, request.user)
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'advanced_dict': json.dumps(CourseMetadata.fetch(course_module)),
'advanced_settings_url': reverse_course_url('advanced_settings_handler', course_key)
})
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET':
return JsonResponse(CourseMetadata.fetch(course_module))
else:
try:
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = _config_course_advanced_components(request, course_module)
# validate data formats and update
is_valid, errors, updated_data = CourseMetadata.validate_and_update_from_json(
course_module,
request.json,
filter_tabs=filter_tabs,
user=request.user,
)
if is_valid:
return JsonResponse(updated_data)
else:
return JsonResponseBadRequest(errors)
# Handle all errors that validation doesn't catch return render_to_response('settings_advanced.html', {
except (TypeError, ValueError) as err: 'context_course': course_module,
return HttpResponseBadRequest( 'advanced_dict': json.dumps(CourseMetadata.fetch(course_module)),
django.utils.html.escape(err.message), 'advanced_settings_url': reverse_course_url('advanced_settings_handler', course_key)
content_type="text/plain" })
) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET':
return JsonResponse(CourseMetadata.fetch(course_module))
else:
try:
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = _config_course_advanced_components(request, course_module)
# validate data formats and update
is_valid, errors, updated_data = CourseMetadata.validate_and_update_from_json(
course_module,
request.json,
filter_tabs=filter_tabs,
user=request.user,
)
if is_valid:
return JsonResponse(updated_data)
else:
return JsonResponseBadRequest(errors)
# Handle all errors that validation doesn't catch
except (TypeError, ValueError) as err:
return HttpResponseBadRequest(
django.utils.html.escape(err.message),
content_type="text/plain"
)
class TextbookValidationError(Exception): class TextbookValidationError(Exception):
...@@ -1004,63 +1010,64 @@ def textbooks_list_handler(request, course_key_string): ...@@ -1004,63 +1010,64 @@ def textbooks_list_handler(request, course_key_string):
json: overwrite all textbooks in the course with the given list json: overwrite all textbooks in the course with the given list
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course = _get_course_module(course_key, request.user)
store = modulestore() store = modulestore()
with store.bulk_operations(course_key):
course = _get_course_module(course_key, request.user)
if not "application/json" in request.META.get('HTTP_ACCEPT', 'text/html'):
# return HTML page
upload_asset_url = reverse_course_url('assets_handler', course_key)
textbook_url = reverse_course_url('textbooks_list_handler', course_key)
return render_to_response('textbooks.html', {
'context_course': course,
'textbooks': course.pdf_textbooks,
'upload_asset_url': upload_asset_url,
'textbook_url': textbook_url,
})
if not "application/json" in request.META.get('HTTP_ACCEPT', 'text/html'): # from here on down, we know the client has requested JSON
# return HTML page if request.method == 'GET':
upload_asset_url = reverse_course_url('assets_handler', course_key) return JsonResponse(course.pdf_textbooks)
textbook_url = reverse_course_url('textbooks_list_handler', course_key) elif request.method == 'PUT':
return render_to_response('textbooks.html', { try:
'context_course': course, textbooks = validate_textbooks_json(request.body)
'textbooks': course.pdf_textbooks, except TextbookValidationError as err:
'upload_asset_url': upload_asset_url, return JsonResponse({"error": err.message}, status=400)
'textbook_url': textbook_url,
})
# from here on down, we know the client has requested JSON tids = set(t["id"] for t in textbooks if "id" in t)
if request.method == 'GET': for textbook in textbooks:
return JsonResponse(course.pdf_textbooks) if not "id" in textbook:
elif request.method == 'PUT': tid = assign_textbook_id(textbook, tids)
try: textbook["id"] = tid
textbooks = validate_textbooks_json(request.body) tids.add(tid)
except TextbookValidationError as err:
return JsonResponse({"error": err.message}, status=400) if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs):
course.tabs.append(PDFTextbookTabs())
tids = set(t["id"] for t in textbooks if "id" in t) course.pdf_textbooks = textbooks
for textbook in textbooks: store.update_item(course, request.user.id)
if not "id" in textbook: return JsonResponse(course.pdf_textbooks)
tid = assign_textbook_id(textbook, tids) elif request.method == 'POST':
textbook["id"] = tid # create a new textbook for the course
tids.add(tid) try:
textbook = validate_textbook_json(request.body)
if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs): except TextbookValidationError as err:
course.tabs.append(PDFTextbookTabs()) return JsonResponse({"error": err.message}, status=400)
course.pdf_textbooks = textbooks if not textbook.get("id"):
store.update_item(course, request.user.id) tids = set(t["id"] for t in course.pdf_textbooks if "id" in t)
return JsonResponse(course.pdf_textbooks) textbook["id"] = assign_textbook_id(textbook, tids)
elif request.method == 'POST': existing = course.pdf_textbooks
# create a new textbook for the course existing.append(textbook)
try: course.pdf_textbooks = existing
textbook = validate_textbook_json(request.body) if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs):
except TextbookValidationError as err: course.tabs.append(PDFTextbookTabs())
return JsonResponse({"error": err.message}, status=400) store.update_item(course, request.user.id)
if not textbook.get("id"): resp = JsonResponse(textbook, status=201)
tids = set(t["id"] for t in course.pdf_textbooks if "id" in t) resp["Location"] = reverse_course_url(
textbook["id"] = assign_textbook_id(textbook, tids) 'textbooks_detail_handler',
existing = course.pdf_textbooks course.id,
existing.append(textbook) kwargs={'textbook_id': textbook["id"]}
course.pdf_textbooks = existing )
if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs): return resp
course.tabs.append(PDFTextbookTabs())
store.update_item(course, request.user.id)
resp = JsonResponse(textbook, status=201)
resp["Location"] = reverse_course_url(
'textbooks_detail_handler',
course.id,
kwargs={'textbook_id': textbook["id"]}
)
return resp
@login_required @login_required
...@@ -1079,45 +1086,46 @@ def textbooks_detail_handler(request, course_key_string, textbook_id): ...@@ -1079,45 +1086,46 @@ def textbooks_detail_handler(request, course_key_string, textbook_id):
json: remove textbook json: remove textbook
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course_module = _get_course_module(course_key, request.user)
store = modulestore() store = modulestore()
matching_id = [tb for tb in course_module.pdf_textbooks with store.bulk_operations(course_key):
if unicode(tb.get("id")) == unicode(textbook_id)] course_module = _get_course_module(course_key, request.user)
if matching_id: matching_id = [tb for tb in course_module.pdf_textbooks
textbook = matching_id[0] if unicode(tb.get("id")) == unicode(textbook_id)]
else: if matching_id:
textbook = None textbook = matching_id[0]
else:
textbook = None
if request.method == 'GET': if request.method == 'GET':
if not textbook: if not textbook:
return JsonResponse(status=404) return JsonResponse(status=404)
return JsonResponse(textbook) return JsonResponse(textbook)
elif request.method in ('POST', 'PUT'): # can be either and sometimes elif request.method in ('POST', 'PUT'): # can be either and sometimes
# django is rewriting one to the other # django is rewriting one to the other
try: try:
new_textbook = validate_textbook_json(request.body) new_textbook = validate_textbook_json(request.body)
except TextbookValidationError as err: except TextbookValidationError as err:
return JsonResponse({"error": err.message}, status=400) return JsonResponse({"error": err.message}, status=400)
new_textbook["id"] = textbook_id new_textbook["id"] = textbook_id
if textbook: if textbook:
i = course_module.pdf_textbooks.index(textbook)
new_textbooks = course_module.pdf_textbooks[0:i]
new_textbooks.append(new_textbook)
new_textbooks.extend(course_module.pdf_textbooks[i + 1:])
course_module.pdf_textbooks = new_textbooks
else:
course_module.pdf_textbooks.append(new_textbook)
store.update_item(course_module, request.user.id)
return JsonResponse(new_textbook, status=201)
elif request.method == 'DELETE':
if not textbook:
return JsonResponse(status=404)
i = course_module.pdf_textbooks.index(textbook) i = course_module.pdf_textbooks.index(textbook)
new_textbooks = course_module.pdf_textbooks[0:i] remaining_textbooks = course_module.pdf_textbooks[0:i]
new_textbooks.append(new_textbook) remaining_textbooks.extend(course_module.pdf_textbooks[i + 1:])
new_textbooks.extend(course_module.pdf_textbooks[i + 1:]) course_module.pdf_textbooks = remaining_textbooks
course_module.pdf_textbooks = new_textbooks store.update_item(course_module, request.user.id)
else: return JsonResponse()
course_module.pdf_textbooks.append(new_textbook)
store.update_item(course_module, request.user.id)
return JsonResponse(new_textbook, status=201)
elif request.method == 'DELETE':
if not textbook:
return JsonResponse(status=404)
i = course_module.pdf_textbooks.index(textbook)
remaining_textbooks = course_module.pdf_textbooks[0:i]
remaining_textbooks.extend(course_module.pdf_textbooks[i + 1:])
course_module.pdf_textbooks = remaining_textbooks
store.update_item(course_module, request.user.id)
return JsonResponse()
class GroupConfigurationsValidationError(Exception): class GroupConfigurationsValidationError(Exception):
...@@ -1314,42 +1322,43 @@ def group_configurations_list_handler(request, course_key_string): ...@@ -1314,42 +1322,43 @@ def group_configurations_list_handler(request, course_key_string):
json: create new group configuration json: create new group configuration
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course = _get_course_module(course_key, request.user)
store = modulestore() store = modulestore()
with store.bulk_operations(course_key):
course = _get_course_module(course_key, request.user)
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
group_configuration_url = reverse_course_url('group_configurations_list_handler', course_key) group_configuration_url = reverse_course_url('group_configurations_list_handler', course_key)
course_outline_url = reverse_course_url('course_handler', course_key) course_outline_url = reverse_course_url('course_handler', course_key)
split_test_enabled = SPLIT_TEST_COMPONENT_TYPE in ADVANCED_COMPONENT_TYPES and SPLIT_TEST_COMPONENT_TYPE in course.advanced_modules split_test_enabled = SPLIT_TEST_COMPONENT_TYPE in ADVANCED_COMPONENT_TYPES and SPLIT_TEST_COMPONENT_TYPE in course.advanced_modules
configurations = GroupConfiguration.add_usage_info(course, store)
return render_to_response('group_configurations.html', {
'context_course': course,
'group_configuration_url': group_configuration_url,
'course_outline_url': course_outline_url,
'configurations': configurations if split_test_enabled else None,
})
elif "application/json" in request.META.get('HTTP_ACCEPT'):
if request.method == 'POST':
# create a new group configuration for the course
try:
new_configuration = GroupConfiguration(request.body, course).get_user_partition()
except GroupConfigurationsValidationError as err:
return JsonResponse({"error": err.message}, status=400)
course.user_partitions.append(new_configuration) configurations = GroupConfiguration.add_usage_info(course, store)
response = JsonResponse(new_configuration.to_json(), status=201)
response["Location"] = reverse_course_url( return render_to_response('group_configurations.html', {
'group_configurations_detail_handler', 'context_course': course,
course.id, 'group_configuration_url': group_configuration_url,
kwargs={'group_configuration_id': new_configuration.id} # pylint: disable=no-member 'course_outline_url': course_outline_url,
) 'configurations': configurations if split_test_enabled else None,
store.update_item(course, request.user.id) })
return response elif "application/json" in request.META.get('HTTP_ACCEPT'):
else: if request.method == 'POST':
return HttpResponse(status=406) # create a new group configuration for the course
try:
new_configuration = GroupConfiguration(request.body, course).get_user_partition()
except GroupConfigurationsValidationError as err:
return JsonResponse({"error": err.message}, status=400)
course.user_partitions.append(new_configuration)
response = JsonResponse(new_configuration.to_json(), status=201)
response["Location"] = reverse_course_url(
'group_configurations_detail_handler',
course.id,
kwargs={'group_configuration_id': new_configuration.id} # pylint: disable=no-member
)
store.update_item(course, request.user.id)
return response
else:
return HttpResponse(status=406)
@login_required @login_required
...@@ -1364,46 +1373,47 @@ def group_configurations_detail_handler(request, course_key_string, group_config ...@@ -1364,46 +1373,47 @@ def group_configurations_detail_handler(request, course_key_string, group_config
json: update group configuration based on provided information json: update group configuration based on provided information
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course = _get_course_module(course_key, request.user)
store = modulestore() store = modulestore()
matching_id = [p for p in course.user_partitions with store.bulk_operations(course_key):
if unicode(p.id) == unicode(group_configuration_id)] course = _get_course_module(course_key, request.user)
if matching_id: matching_id = [p for p in course.user_partitions
configuration = matching_id[0] if unicode(p.id) == unicode(group_configuration_id)]
else: if matching_id:
configuration = None configuration = matching_id[0]
else:
configuration = None
if request.method in ('POST', 'PUT'): # can be either and sometimes if request.method in ('POST', 'PUT'): # can be either and sometimes
# django is rewriting one to the other # django is rewriting one to the other
try: try:
new_configuration = GroupConfiguration(request.body, course, group_configuration_id).get_user_partition() new_configuration = GroupConfiguration(request.body, course, group_configuration_id).get_user_partition()
except GroupConfigurationsValidationError as err: except GroupConfigurationsValidationError as err:
return JsonResponse({"error": err.message}, status=400) return JsonResponse({"error": err.message}, status=400)
if configuration: if configuration:
index = course.user_partitions.index(configuration) index = course.user_partitions.index(configuration)
course.user_partitions[index] = new_configuration course.user_partitions[index] = new_configuration
else: else:
course.user_partitions.append(new_configuration) course.user_partitions.append(new_configuration)
store.update_item(course, request.user.id) store.update_item(course, request.user.id)
configuration = GroupConfiguration.update_usage_info(store, course, new_configuration) configuration = GroupConfiguration.update_usage_info(store, course, new_configuration)
return JsonResponse(configuration, status=201) return JsonResponse(configuration, status=201)
elif request.method == "DELETE": elif request.method == "DELETE":
if not configuration: if not configuration:
return JsonResponse(status=404) return JsonResponse(status=404)
# Verify that group configuration is not already in use. # Verify that group configuration is not already in use.
usages = GroupConfiguration.get_usage_info(course, store) usages = GroupConfiguration.get_usage_info(course, store)
if usages.get(int(group_configuration_id)): if usages.get(int(group_configuration_id)):
return JsonResponse( return JsonResponse(
{"error": _("This Group Configuration is already in use and cannot be removed.")}, {"error": _("This Group Configuration is already in use and cannot be removed.")},
status=400 status=400
) )
index = course.user_partitions.index(configuration) index = course.user_partitions.index(configuration)
course.user_partitions.pop(index) course.user_partitions.pop(index)
store.update_item(course, request.user.id) store.update_item(course, request.user.id)
return JsonResponse(status=204) return JsonResponse(status=204)
def _get_course_creator_status(user): def _get_course_creator_status(user):
......
...@@ -461,54 +461,55 @@ def _create_item(request): ...@@ -461,54 +461,55 @@ def _create_item(request):
raise PermissionDenied() raise PermissionDenied()
store = modulestore() store = modulestore()
parent = store.get_item(usage_key) with store.bulk_operations(usage_key.course_key):
dest_usage_key = usage_key.replace(category=category, name=uuid4().hex) parent = store.get_item(usage_key)
dest_usage_key = usage_key.replace(category=category, name=uuid4().hex)
# get the metadata, display_name, and definition from the request
metadata = {} # get the metadata, display_name, and definition from the request
data = None metadata = {}
template_id = request.json.get('boilerplate') data = None
if template_id: template_id = request.json.get('boilerplate')
clz = parent.runtime.load_block_type(category) if template_id:
if clz is not None: clz = parent.runtime.load_block_type(category)
template = clz.get_template(template_id) if clz is not None:
if template is not None: template = clz.get_template(template_id)
metadata = template.get('metadata', {}) if template is not None:
data = template.get('data') metadata = template.get('metadata', {})
data = template.get('data')
if display_name is not None:
metadata['display_name'] = display_name if display_name is not None:
metadata['display_name'] = display_name
# TODO need to fix components that are sending definition_data as strings, instead of as dicts
# For now, migrate them into dicts here. # TODO need to fix components that are sending definition_data as strings, instead of as dicts
if isinstance(data, basestring): # For now, migrate them into dicts here.
data = {'data': data} if isinstance(data, basestring):
data = {'data': data}
created_block = store.create_child(
request.user.id, created_block = store.create_child(
usage_key, request.user.id,
dest_usage_key.block_type, usage_key,
block_id=dest_usage_key.block_id, dest_usage_key.block_type,
definition_data=data, block_id=dest_usage_key.block_id,
metadata=metadata, definition_data=data,
runtime=parent.runtime, metadata=metadata,
) runtime=parent.runtime,
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata)
# we should remove this once we can break this reference from the course to static tabs
if category == 'static_tab':
display_name = display_name or _("Empty") # Prevent name being None
course = store.get_course(dest_usage_key.course_key)
course.tabs.append(
StaticTab(
name=display_name,
url_slug=dest_usage_key.name,
)
) )
store.update_item(course, request.user.id)
return JsonResponse({"locator": unicode(created_block.location), "courseKey": unicode(created_block.location.course_key)}) # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata)
# we should remove this once we can break this reference from the course to static tabs
if category == 'static_tab':
display_name = display_name or _("Empty") # Prevent name being None
course = store.get_course(dest_usage_key.course_key)
course.tabs.append(
StaticTab(
name=display_name,
url_slug=dest_usage_key.name,
)
)
store.update_item(course, request.user.id)
return JsonResponse({"locator": unicode(created_block.location), "courseKey": unicode(created_block.location.course_key)})
def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_name=None): def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_name=None):
...@@ -516,52 +517,53 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_ ...@@ -516,52 +517,53 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
Duplicate an existing xblock as a child of the supplied parent_usage_key. Duplicate an existing xblock as a child of the supplied parent_usage_key.
""" """
store = modulestore() store = modulestore()
source_item = store.get_item(duplicate_source_usage_key) with store.bulk_operations(duplicate_source_usage_key.course_key):
# Change the blockID to be unique. source_item = store.get_item(duplicate_source_usage_key)
dest_usage_key = source_item.location.replace(name=uuid4().hex) # Change the blockID to be unique.
category = dest_usage_key.block_type dest_usage_key = source_item.location.replace(name=uuid4().hex)
category = dest_usage_key.block_type
# Update the display name to indicate this is a duplicate (unless display name provided).
duplicate_metadata = own_metadata(source_item) # Update the display name to indicate this is a duplicate (unless display name provided).
if display_name is not None: duplicate_metadata = own_metadata(source_item)
duplicate_metadata['display_name'] = display_name if display_name is not None:
else: duplicate_metadata['display_name'] = display_name
if source_item.display_name is None:
duplicate_metadata['display_name'] = _("Duplicate of {0}").format(source_item.category)
else: else:
duplicate_metadata['display_name'] = _("Duplicate of '{0}'").format(source_item.display_name) if source_item.display_name is None:
duplicate_metadata['display_name'] = _("Duplicate of {0}").format(source_item.category)
dest_module = store.create_item( else:
user.id, duplicate_metadata['display_name'] = _("Duplicate of '{0}'").format(source_item.display_name)
dest_usage_key.course_key,
dest_usage_key.block_type, dest_module = store.create_item(
block_id=dest_usage_key.block_id, user.id,
definition_data=source_item.get_explicitly_set_fields_by_scope(Scope.content), dest_usage_key.course_key,
metadata=duplicate_metadata, dest_usage_key.block_type,
runtime=source_item.runtime, block_id=dest_usage_key.block_id,
) definition_data=source_item.get_explicitly_set_fields_by_scope(Scope.content),
metadata=duplicate_metadata,
# Children are not automatically copied over (and not all xblocks have a 'children' attribute). runtime=source_item.runtime,
# Because DAGs are not fully supported, we need to actually duplicate each child as well. )
if source_item.has_children:
dest_module.children = []
for child in source_item.children:
dupe = _duplicate_item(dest_module.location, child, user=user)
dest_module.children.append(dupe)
store.update_item(dest_module, user.id)
if 'detached' not in source_item.runtime.load_block_type(category)._class_tags:
parent = store.get_item(parent_usage_key)
# If source was already a child of the parent, add duplicate immediately afterward.
# Otherwise, add child to end.
if source_item.location in parent.children:
source_index = parent.children.index(source_item.location)
parent.children.insert(source_index + 1, dest_module.location)
else:
parent.children.append(dest_module.location)
store.update_item(parent, user.id)
return dest_module.location # Children are not automatically copied over (and not all xblocks have a 'children' attribute).
# Because DAGs are not fully supported, we need to actually duplicate each child as well.
if source_item.has_children:
dest_module.children = []
for child in source_item.children:
dupe = _duplicate_item(dest_module.location, child, user=user)
dest_module.children.append(dupe)
store.update_item(dest_module, user.id)
if 'detached' not in source_item.runtime.load_block_type(category)._class_tags:
parent = store.get_item(parent_usage_key)
# If source was already a child of the parent, add duplicate immediately afterward.
# Otherwise, add child to end.
if source_item.location in parent.children:
source_index = parent.children.index(source_item.location)
parent.children.insert(source_index + 1, dest_module.location)
else:
parent.children.append(dest_module.location)
store.update_item(parent, user.id)
return dest_module.location
def _delete_item(usage_key, user): def _delete_item(usage_key, user):
...@@ -571,16 +573,17 @@ def _delete_item(usage_key, user): ...@@ -571,16 +573,17 @@ def _delete_item(usage_key, user):
""" """
store = modulestore() store = modulestore()
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so with store.bulk_operations(usage_key.course_key):
# if we add one then we need to also add it to the policy information (i.e. metadata) # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# we should remove this once we can break this reference from the course to static tabs # if we add one then we need to also add it to the policy information (i.e. metadata)
if usage_key.category == 'static_tab': # we should remove this once we can break this reference from the course to static tabs
course = store.get_course(usage_key.course_key) if usage_key.category == 'static_tab':
existing_tabs = course.tabs or [] course = store.get_course(usage_key.course_key)
course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != usage_key.name] existing_tabs = course.tabs or []
store.update_item(course, user.id) course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != usage_key.name]
store.update_item(course, user.id)
store.delete_item(usage_key, user.id) store.delete_item(usage_key, user.id)
# pylint: disable=W0613 # pylint: disable=W0613
...@@ -618,17 +621,18 @@ def _get_xblock(usage_key, user): ...@@ -618,17 +621,18 @@ def _get_xblock(usage_key, user):
in the CREATE_IF_NOT_FOUND list, an xblock will be created and saved automatically. in the CREATE_IF_NOT_FOUND list, an xblock will be created and saved automatically.
""" """
store = modulestore() store = modulestore()
try: with store.bulk_operations(usage_key.course_key):
return store.get_item(usage_key, depth=None) try:
except ItemNotFoundError: return store.get_item(usage_key, depth=None)
if usage_key.category in CREATE_IF_NOT_FOUND: except ItemNotFoundError:
# Create a new one for certain categories only. Used for course info handouts. if usage_key.category in CREATE_IF_NOT_FOUND:
return store.create_item(user.id, usage_key.course_key, usage_key.block_type, block_id=usage_key.block_id) # Create a new one for certain categories only. Used for course info handouts.
else: return store.create_item(user.id, usage_key.course_key, usage_key.block_type, block_id=usage_key.block_id)
raise else:
except InvalidLocationError: raise
log.error("Can't find item by location.") except InvalidLocationError:
return JsonResponse({"error": "Can't find item by location: " + unicode(usage_key)}, 404) log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location: " + unicode(usage_key)}, 404)
def _get_module_info(xblock, rewrite_static_links=True): def _get_module_info(xblock, rewrite_static_links=True):
...@@ -636,19 +640,20 @@ def _get_module_info(xblock, rewrite_static_links=True): ...@@ -636,19 +640,20 @@ def _get_module_info(xblock, rewrite_static_links=True):
metadata, data, id representation of a leaf module fetcher. metadata, data, id representation of a leaf module fetcher.
:param usage_key: A UsageKey :param usage_key: A UsageKey
""" """
data = getattr(xblock, 'data', '') with modulestore().bulk_operations(xblock.location.course_key):
if rewrite_static_links: data = getattr(xblock, 'data', '')
data = replace_static_urls( if rewrite_static_links:
data, data = replace_static_urls(
None, data,
course_id=xblock.location.course_key None,
) course_id=xblock.location.course_key
)
# Pre-cache has changes for the entire course because we'll need it for the ancestor info # Pre-cache has changes for the entire course because we'll need it for the ancestor info
modulestore().has_changes(modulestore().get_course(xblock.location.course_key, depth=None)) modulestore().has_changes(modulestore().get_course(xblock.location.course_key, depth=None))
# Note that children aren't being returned until we have a use case. # Note that children aren't being returned until we have a use case.
return create_xblock_info(xblock, data=data, metadata=own_metadata(xblock), include_ancestor_info=True) return create_xblock_info(xblock, data=data, metadata=own_metadata(xblock), include_ancestor_info=True)
def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False, def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False,
......
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