def test_get_all_users(self): """ Test getting all authors for a course where their permissions run the gamut of allowed group types. """ # first check the course creator.has explicit access (don't use has_access as is_staff # will trump the actual test) self.assertTrue(CourseInstructorRole(self.course_key).has_user(self.user), "Didn't add creator as instructor.") users = copy.copy(self.users) # doesn't use role.users_with_role b/c it's verifying the roles.py behavior user_by_role = {} # add the misc users to the course in different groups for role in [CourseInstructorRole, CourseStaffRole, OrgStaffRole, OrgInstructorRole]: user_by_role[role] = [] # Org-based roles are created via org name, rather than course_key if (role is OrgStaffRole) or (role is OrgInstructorRole): group = role(self.course_key.org) else: group = role(self.course_key) # NOTE: this loop breaks the roles.py abstraction by purposely assigning # users to one of each possible groupname in order to test that has_course_author_access # and remove_user work user = users.pop() group.add_users(user) user_by_role[role].append(user) self.assertTrue( auth.has_course_author_access(user, self.course_key), "{} does not have access".format(user) ) course_team_url = reverse_course_url("course_team_handler", self.course_key) response = self.client.get_html(course_team_url) for role in [CourseInstructorRole, CourseStaffRole]: # Global and org-based roles don't appear on this page for user in user_by_role[role]: self.assertContains(response, user.email) # test copying course permissions copy_course_key = self.store.make_course_key("copyu", "copydept.mycourse", "myrun") for role in [CourseInstructorRole, CourseStaffRole, OrgStaffRole, OrgInstructorRole]: if (role is OrgStaffRole) or (role is OrgInstructorRole): auth.add_users(self.user, role(copy_course_key.org), *role(self.course_key.org).users_with_role()) else: auth.add_users(self.user, role(copy_course_key), *role(self.course_key).users_with_role()) # verify access in copy course and verify that removal from source course w/ the various # groupnames works for role in [CourseInstructorRole, CourseStaffRole, OrgStaffRole, OrgInstructorRole]: for user in user_by_role[role]: # forcefully decache the groups: premise is that any real request will not have # multiple objects repr the same user but this test somehow uses different instance # in above add_users call if hasattr(user, "_roles"): del user._roles self.assertTrue(auth.has_course_author_access(user, copy_course_key), "{} no copy access".format(user)) if (role is OrgStaffRole) or (role is OrgInstructorRole): auth.remove_users(self.user, role(self.course_key.org), user) else: auth.remove_users(self.user, role(self.course_key), user) self.assertFalse( auth.has_course_author_access(user, self.course_key), "{} remove didn't work".format(user) )
def _manage_users(request, course_key): """ This view will return all CMS users who are editors for the specified course """ # check that logged in user has permissions to this item if not has_course_author_access(request.user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key) instructors = CourseInstructorRole(course_key).users_with_role() # the page only lists staff and assumes they're a superset of instructors. Do a union to ensure. staff = set( CourseStaffRole(course_key).users_with_role()).union(instructors) return render_to_response( 'manage_users.html', { 'context_course': course_module, 'staff': staff, 'instructors': instructors, 'allow_actions': has_course_author_access( request.user, course_key, role=CourseInstructorRole), })
def get(self, request, course_id): """ Check the status of the specified task """ courselike_key = CourseKey.from_string(course_id) if not has_course_author_access(request.user, courselike_key): return self.make_error_response( status_code=status.HTTP_403_FORBIDDEN, developer_message='The user requested does not have the required permissions.', error_code='user_mismatch' ) try: task_id = request.GET['task_id'] filename = request.GET['filename'] args = {u'course_key_string': course_id, u'archive_name': filename} name = CourseImportTask.generate_name(args) task_status = UserTaskStatus.objects.filter(name=name, task_id=task_id).first() return Response({ 'state': task_status.state }) except Exception as e: return self.make_error_response( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, developer_message=str(e), error_code='internal_error' )
def export_output_handler(request, course_key_string): """ Returns the OLX .tar.gz produced by a file export. Only used in environments such as devstack where the output is stored in a local filesystem instead of an external service like S3. """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() task_status = _latest_task_status(request, course_key_string, export_output_handler) if task_status and task_status.state == UserTaskStatus.SUCCEEDED: artifact = None try: artifact = UserTaskArtifact.objects.get(status=task_status, name='Output') tarball = course_import_export_storage.open(artifact.file.name) return send_tarball(tarball, artifact.file.storage.size(artifact.file.name)) except UserTaskArtifact.DoesNotExist: raise Http404 finally: if artifact: artifact.file.close() else: raise Http404
def test_notifications_handler_dismiss(self): state = CourseRerunUIStateManager.State.FAILED should_display = True rerun_course_key = CourseLocator(org='testx', course='test_course', run='test_run') # add an instructor to this course user2 = UserFactory() add_instructor(rerun_course_key, self.user, user2) # create a test notification rerun_state = CourseRerunState.objects.update_state( course_key=rerun_course_key, new_state=state, allow_not_found=True ) CourseRerunState.objects.update_should_display( entry_id=rerun_state.id, user=user2, should_display=should_display ) # try to get information on this notification notification_dismiss_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={ 'action_state_id': rerun_state.id, }) resp = self.client.delete(notification_dismiss_url) self.assertEquals(resp.status_code, 200) with self.assertRaises(CourseRerunState.DoesNotExist): # delete nofications that are dismissed CourseRerunState.objects.get(id=rerun_state.id) self.assertFalse(has_course_author_access(user2, rerun_course_key))
def check_course_access(course_key, user=None, ip_address=None, url=None): """ Check is the user with this ip_address has access to the given course Arguments: course_key (CourseKey): Location of the course the user is trying to access. Keyword Arguments: user (User): The user making the request. Can be None, in which case the user's profile country will not be checked. ip_address (str): The IP address of the request. url (str): The URL the user is trying to access. Used in log messages. Returns: Boolean: True if the user has access to the course; False otherwise """ # No-op if the country access feature is not enabled if not settings.FEATURES.get('EMBARGO'): return True # First, check whether there are any restrictions on the course. # If not, then we do not need to do any further checks course_is_restricted = RestrictedCourse.is_restricted_course(course_key) if not course_is_restricted: return True # Always give global and course staff access, regardless of embargo settings. if user is not None and has_course_author_access(user, course_key): return True if ip_address is not None: # Retrieve the country code from the IP address # and check it against the allowed countries list for a course user_country_from_ip = _country_code_from_ip(ip_address) if not CountryAccessRule.check_country_access(course_key, user_country_from_ip): log.info((u"Blocking user %s from accessing course %s at %s " u"because the user's IP address %s appears to be " u"located in %s."), getattr(user, 'id', '<Not Authenticated>'), course_key, url, ip_address, user_country_from_ip) return False if user is not None: # Retrieve the country code from the user's profile # and check it against the allowed countries list for a course. user_country_from_profile = _get_user_country_from_profile(user) if not CountryAccessRule.check_country_access( course_key, user_country_from_profile): log.info((u"Blocking user %s from accessing course %s at %s " u"because the user's profile country is %s."), user.id, course_key, url, user_country_from_profile) return False return True
def test_notifications_handler_dismiss(self): state = CourseRerunUIStateManager.State.FAILED should_display = True rerun_course_key = CourseLocator(org='testx', course='test_course', run='test_run') # add an instructor to this course user2 = UserFactory() add_instructor(rerun_course_key, self.user, user2) # create a test notification rerun_state = CourseRerunState.objects.update_state( course_key=rerun_course_key, new_state=state, allow_not_found=True) CourseRerunState.objects.update_should_display( entry_id=rerun_state.id, user=user2, should_display=should_display) # try to get information on this notification notification_dismiss_url = reverse_course_url( 'course_notifications_handler', self.course.id, kwargs={ 'action_state_id': rerun_state.id, }) resp = self.client.delete(notification_dismiss_url) self.assertEquals(resp.status_code, 200) with self.assertRaises(CourseRerunState.DoesNotExist): # delete nofications that are dismissed CourseRerunState.objects.get(id=rerun_state.id) self.assertFalse(has_course_author_access(user2, rerun_course_key))
def orphan_handler(request, course_key_string): """ View for handling orphan related requests. GET gets all of the current orphans. DELETE removes all orphans (requires is_staff access) An orphan is a block whose category is not in the DETACHED_CATEGORY list, is not the root, and is not reachable from the root via children """ course_usage_key = CourseKey.from_string(course_key_string) if request.method == 'GET': if has_course_author_access(request.user, course_usage_key): return JsonResponse([ unicode(item) for item in modulestore().get_orphans(course_usage_key) ]) else: raise PermissionDenied() if request.method == 'DELETE': if request.user.is_staff: deleted_items = _delete_orphans(course_usage_key, request.user.id, commit=True) return JsonResponse({'deleted': deleted_items}) else: raise PermissionDenied()
def utility_handler(request, course_key_string): """ The restful handler for utilities. GET html: return html page for all utilities json: return json representing all utilities. """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key) json_request = 'application/json' in request.META.get( 'HTTP_ACCEPT', 'application/json') if request.method == 'GET': expanded_utilities = expand_all_action_urls(course_module) if json_request: return JsonResponse(expanded_utilities) else: handler_url = reverse_course_url('utility_handler', course_module.id) return render_to_response( 'utilities.html', { 'handler_url': handler_url, 'context_course': course_module, 'utilities': expanded_utilities }) else: # return HttpResponseNotFound() raise NotImplementedError()
def utility_handler(request, course_key_string): """ The restful handler for utilities. GET html: return html page for all utilities json: return json representing all utilities. """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key) json_request = 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json') if request.method == 'GET': expanded_utilities = expand_all_action_urls(course_module) if json_request: return JsonResponse(expanded_utilities) else: handler_url = reverse_course_url('utility_handler', course_module.id) return render_to_response('utilities.html', { 'handler_url': handler_url, 'context_course': course_module, 'utilities': expanded_utilities }) else: # return HttpResponseNotFound() raise NotImplementedError()
def export_olx(self, user_id, course_key_string, language): """ Export a course or library to an OLX .tar.gz archive and prepare it for download. """ courselike_key = CourseKey.from_string(course_key_string) try: user = User.objects.get(pk=user_id) except User.DoesNotExist: with respect_language(language): self.status.fail(_(u'Unknown User ID: {0}').format(user_id)) return if not has_course_author_access(user, courselike_key): with respect_language(language): self.status.fail(_(u'Permission denied')) return if isinstance(courselike_key, LibraryLocator): courselike_module = modulestore().get_library(courselike_key) else: courselike_module = modulestore().get_course(courselike_key) try: self.status.set_state(u'Exporting') tarball = create_export_tarball(courselike_module, courselike_key, {}, self.status) artifact = UserTaskArtifact(status=self.status, name=u'Output') artifact.file.save(name=tarball.name, content=File(tarball)) # pylint: disable=no-member artifact.save() # catch all exceptions so we can record useful error messages except Exception as exception: # pylint: disable=broad-except LOGGER.exception(u'Error exporting course %s', courselike_key) if self.status.state != UserTaskStatus.FAILED: self.status.fail({'raw_error_msg': text_type(exception)}) return
def orphan_handler(request, course_key_string): """ View for handling orphan related requests. GET gets all of the current orphans. DELETE removes all orphans (requires is_staff access) An orphan is a block whose category is not in the DETACHED_CATEGORY list, is not the root, and is not reachable from the root via children """ course_usage_key = CourseKey.from_string(course_key_string) if request.method == 'GET': if has_course_author_access(request.user, course_usage_key): return JsonResponse([ unicode(item) for item in modulestore().get_orphans(course_usage_key) ]) else: raise PermissionDenied() if request.method == 'DELETE': if request.user.is_staff: store = modulestore() items = store.get_orphans(course_usage_key) for itemloc in items: # need to delete all versions store.delete_item(itemloc, request.user.id, revision=ModuleStoreEnum.RevisionOption.all) return JsonResponse({'deleted': [unicode(item) for item in items]}) else: raise PermissionDenied()
def export_olx(self, user_id, course_key_string, language): """ Export a course or library to an OLX .tar.gz archive and prepare it for download. """ courselike_key = CourseKey.from_string(course_key_string) try: user = User.objects.get(pk=user_id) except User.DoesNotExist: with respect_language(language): self.status.fail(_(u'Unknown User ID: {0}').format(user_id)) return if not has_course_author_access(user, courselike_key): with respect_language(language): self.status.fail(_(u'Permission denied')) return if isinstance(courselike_key, LibraryLocator): courselike_module = modulestore().get_library(courselike_key) else: courselike_module = modulestore().get_course(courselike_key) try: self.status.set_state(u'Exporting') tarball = create_export_tarball(courselike_module, courselike_key, {}, self.status) artifact = UserTaskArtifact(status=self.status, name=u'Output') artifact.file.save(name=os.path.basename(tarball.name), content=File(tarball)) # pylint: disable=no-member artifact.save() # catch all exceptions so we can record useful error messages except Exception as exception: # pylint: disable=broad-except LOGGER.exception(u'Error exporting course %s', courselike_key, exc_info=True) if self.status.state != UserTaskStatus.FAILED: self.status.fail({'raw_error_msg': text_type(exception)}) return
def _create_item(request): """View for create items.""" usage_key = usage_key_with_run(request.json['parent_locator']) category = request.json['category'] display_name = request.json.get('display_name') if not has_course_author_access(request.user, usage_key.course_key): raise PermissionDenied() store = modulestore() with store.bulk_operations(usage_key.course_key): 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 = {} data = None template_id = request.json.get('boilerplate') if template_id: clz = parent.runtime.load_block_type(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') 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. if isinstance(data, basestring): data = {'data': data} created_block = store.create_child( request.user.id, usage_key, dest_usage_key.block_type, block_id=dest_usage_key.block_id, definition_data=data, 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)})
def export_git(request, course_key_string): """ This method serves up the 'Export to Git' page """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key) failed = False log.debug('export_git course_module=%s', course_module) msg = "" if 'action' in request.GET and course_module.giturl: if request.GET['action'] == 'push': try: git_export_utils.export_to_git( course_module.id, course_module.giturl, request.user, ) msg = _('Course successfully exported to git repository') except git_export_utils.GitExportError as ex: failed = True msg = unicode(ex) return render_to_response('export_git.html', { 'context_course': course_module, 'msg': msg, 'failed': failed, })
def export_handler(request, course_key_string): """ The restful handler for exporting a course. GET html: return html page for import page application/x-tgz: return tar.gz file containing exported course json: not supported Note that there are 2 ways to request the tar.gz file. The request header can specify application/x-tgz via HTTP_ACCEPT, or a query parameter can be used (?_accept=application/x-tgz). If the tar.gz file has been requested but the export operation fails, an HTML page will be returned which describes the error. """ course_key = CourseKey.from_string(course_key_string) export_url = reverse_course_url('export_handler', course_key) if not has_course_author_access(request.user, course_key): raise PermissionDenied() if isinstance(course_key, LibraryLocator): courselike_module = modulestore().get_library(course_key) context = { 'context_library': courselike_module, 'courselike_home_url': reverse_library_url("library_handler", course_key), 'library': True } else: courselike_module = modulestore().get_course(course_key) context = { 'context_course': courselike_module, 'courselike_home_url': reverse_course_url("course_handler", course_key), 'library': False } context['export_url'] = export_url + '?_accept=application/x-tgz' # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header. requested_format = request.REQUEST.get( '_accept', request.META.get('HTTP_ACCEPT', 'text/html')) if 'application/x-tgz' in requested_format: try: tarball = create_export_tarball(courselike_module, course_key, context) except SerializationError: return render_to_response('export.html', context) return send_tarball(tarball) elif 'text/html' in requested_format: return render_to_response('export.html', context) else: # Only HTML or x-tgz request formats are supported (no JSON). return HttpResponse(status=406)
def has_permission(self, request, view): course_key_string = view.kwargs['course_key_string'] try: course_key = CourseKey.from_string(course_key_string) except InvalidKeyError: raise Http404 return has_course_author_access(request.user, course_key)
def editorjs_handler(request, course_key_string=None, asset_key_string=None): ''' The restful handler for assets. It allows retrieval of all the assets (as an HTML page), as well as uploading new assets, deleting assets, and changing the 'locked' state of an asset. GET html: return an html page which will show all course assets. Note that only the asset container is returned and that the actual assets are filled in with a client-side request. json: returns a page of assets. The following parameters are supported: page: the desired page of results (defaults to 0) page_size: the number of items per page (defaults to 50) sort: the asset field to sort by (defaults to 'date_added') direction: the sort direction (defaults to 'descending') asset_type: the file type to filter items to (defaults to All) text_search: string to filter results by file name (defaults to '') POST json: create (or update?) an asset. The only updating that can be done is changing the lock state. PUT json: update the locked state of an asset DELETE json: delete an asset ''' course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() response_format = _get_response_format(request) if _request_response_format_is_json(request, response_format): if request.method == 'GET': if request.method == 'GET' and request.GET.get('url'): url = request.GET['url'] return JsonResponse({ "success": 1, "meta": { 'url': url, 'display_name': url, 'content_type': url, 'date_added': url, 'asset_url': url, 'external_url': url, 'portable_url': url, 'thumbnail': url, 'locked': url, 'id': url } }) else: return _assets_json(request, course_key) asset_key = AssetKey.from_string( asset_key_string) if asset_key_string else None return _update_asset(request, course_key, asset_key) elif request.method == 'GET': # assume html return _asset_index(request, course_key) return HttpResponseNotFound()
def tabs_handler(request, course_key_string): """ The restful handler for static tabs. GET html: return page for editing static tabs json: not supported PUT or POST json: update the tab order. It is expected that the request body contains a JSON-encoded dict with entry "tabs". The value for "tabs" is an array of tab locators, indicating the desired order of the tabs. Creating a tab, deleting a tab, or changing its contents is not supported through this method. Instead use the general xblock URL (see item.xblock_handler). """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() course_item = modulestore().get_course(course_key) if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if request.method == 'GET': raise NotImplementedError('coming soon') else: if 'tabs' in request.json: return reorder_tabs_handler(course_item, request) elif 'tab_id_locator' in request.json: return edit_tab_handler(course_item, request) else: raise NotImplementedError( 'Creating or changing tab content is not supported.') elif request.method == 'GET': # assume html # get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs # present in the same order they are displayed in LMS tabs_to_render = [] for tab in CourseTabList.iterate_displayable(course_item, user=request.user, inline_collections=False, include_hidden=True): if isinstance(tab, StaticTab): # static tab needs its locator information to render itself as an xmodule static_tab_loc = course_key.make_usage_key( 'static_tab', tab.url_slug) tab.locator = static_tab_loc tabs_to_render.append(tab) return render_to_response( 'edit-tabs.html', { 'context_course': course_item, 'tabs_to_render': tabs_to_render, 'lms_link': get_lms_link_for_item(course_item.location), }) else: return HttpResponseNotFound()
def test_rerun_course(self): """ Unit tests for :meth: `contentstore.tasks.rerun_course` """ mongo_course1_id = self.import_and_populate_course() # rerun from mongo into split split_course3_id = CourseLocator(org="edx3", course="split3", run="rerun_test") # Mark the action as initiated fields = {'display_name': 'rerun'} CourseRerunState.objects.initiated(mongo_course1_id, split_course3_id, self.user, fields['display_name']) result = rerun_course.delay(six.text_type(mongo_course1_id), six.text_type(split_course3_id), self.user.id, json.dumps(fields, cls=EdxJSONEncoder)) self.assertEqual(result.get(), "succeeded") self.assertTrue(has_course_author_access(self.user, split_course3_id), "Didn't grant access") rerun_state = CourseRerunState.objects.find_first( course_key=split_course3_id) self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.SUCCEEDED) # try creating rerunning again to same name and ensure it generates error result = rerun_course.delay(six.text_type(mongo_course1_id), six.text_type(split_course3_id), self.user.id) self.assertEqual(result.get(), "duplicate course") # the below will raise an exception if the record doesn't exist CourseRerunState.objects.find_first( course_key=split_course3_id, state=CourseRerunUIStateManager.State.FAILED) # try to hit the generic exception catch with patch( 'xmodule.modulestore.split_mongo.mongo_connection.MongoConnection.insert_course_index', Mock(side_effect=Exception)): split_course4_id = CourseLocator(org="edx3", course="split3", run="rerun_fail") fields = {'display_name': 'total failure'} CourseRerunState.objects.initiated(split_course3_id, split_course4_id, self.user, fields['display_name']) result = rerun_course.delay(six.text_type(split_course3_id), six.text_type(split_course4_id), self.user.id, json.dumps(fields, cls=EdxJSONEncoder)) self.assertIn("exception: ", result.get()) self.assertIsNone(self.store.get_course(split_course4_id), "Didn't delete course after error") CourseRerunState.objects.find_first( course_key=split_course4_id, state=CourseRerunUIStateManager.State.FAILED)
def post(self, request, course_id): """ Kicks off an asynchronous course import and returns an ID to be used to check the task's status """ courselike_key = CourseKey.from_string(course_id) if not has_course_author_access(request.user, courselike_key): return self.make_error_response( status_code=status.HTTP_403_FORBIDDEN, developer_message='The user requested does not have the required permissions.', error_code='user_mismatch' ) try: if 'course_data' not in request.FILES: return self.make_error_response( status_code=status.HTTP_400_BAD_REQUEST, developer_message='Missing required parameter', error_code='internal_error', field_errors={'course_data': '"course_data" parameter is required, and must be a .tar.gz file'} ) filename = request.FILES['course_data'].name if not filename.endswith('.tar.gz'): return self.make_error_response( status_code=status.HTTP_400_BAD_REQUEST, developer_message='Parameter in the wrong format', error_code='internal_error', field_errors={'course_data': '"course_data" parameter is required, and must be a .tar.gz file'} ) course_dir = path(settings.GITHUB_REPO_ROOT) / base64.urlsafe_b64encode(repr(courselike_key)) temp_filepath = course_dir / filename if not course_dir.isdir(): # pylint: disable=no-value-for-parameter os.mkdir(course_dir) log.debug('importing course to {0}'.format(temp_filepath)) with open(temp_filepath, "wb+") as temp_file: for chunk in request.FILES['course_data'].chunks(): temp_file.write(chunk) log.info("Course import %s: Upload complete", courselike_key) with open(temp_filepath, 'rb') as local_file: django_file = File(local_file) storage_path = course_import_export_storage.save(u'olx_import/' + filename, django_file) async_result = import_olx.delay( request.user.id, text_type(courselike_key), storage_path, filename, request.LANGUAGE_CODE) return Response({ 'task_id': async_result.task_id }) except Exception as e: return self.make_error_response( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, developer_message=str(e), error_code='internal_error' )
def export_course_single(user, plugin_class, tempdir, course_key): """ Generate a single export file and return a path to it and its name. """ if not has_course_author_access(user, course_key): raise PermissionDenied() (outfilepath, out_fn) = _do_course_export(plugin_class, tempdir, course_key) return (outfilepath, out_fn)
def _manage_users(request, course_key): """ This view will return all CMS users who are editors for the specified course """ # check that logged in user has permissions to this item if not has_course_author_access(request.user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key) instructors = CourseInstructorRole(course_key).users_with_role() # the page only lists staff and assumes they're a superset of instructors. Do a union to ensure. staff = set(CourseStaffRole(course_key).users_with_role()).union(instructors) return render_to_response('manage_users.html', { 'context_course': course_module, 'staff': staff, 'instructors': instructors, 'allow_actions': has_course_author_access(request.user, course_key, role=CourseInstructorRole), })
def export_handler(request, course_key_string): """ The restful handler for exporting a course. GET html: return html page for import page application/x-tgz: return tar.gz file containing exported course json: not supported Note that there are 2 ways to request the tar.gz file. The request header can specify application/x-tgz via HTTP_ACCEPT, or a query parameter can be used (?_accept=application/x-tgz). If the tar.gz file has been requested but the export operation fails, an HTML page will be returned which describes the error. """ course_key = CourseKey.from_string(course_key_string) export_url = reverse_course_url('export_handler', course_key) if not has_course_author_access(request.user, course_key): raise PermissionDenied() if isinstance(course_key, LibraryLocator): courselike_module = modulestore().get_library(course_key) context = { 'context_library': courselike_module, 'courselike_home_url': reverse_library_url("library_handler", course_key), 'library': True } else: courselike_module = modulestore().get_course(course_key) if courselike_module is None: raise Http404 context = { 'context_course': courselike_module, 'courselike_home_url': reverse_course_url("course_handler", course_key), 'library': False } context['export_url'] = export_url + '?_accept=application/x-tgz' # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header. requested_format = request.GET.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html')) if 'application/x-tgz' in requested_format: try: tarball = create_export_tarball(courselike_module, course_key, context) except SerializationError: return render_to_response('export.html', context) return send_tarball(tarball) elif 'text/html' in requested_format: return render_to_response('export.html', context) else: # Only HTML or x-tgz request formats are supported (no JSON). return HttpResponse(status=406)
def tabs_handler(request, course_key_string): """ The restful handler for static tabs. GET html: return page for editing static tabs json: not supported PUT or POST json: update the tab order. It is expected that the request body contains a JSON-encoded dict with entry "tabs". The value for "tabs" is an array of tab locators, indicating the desired order of the tabs. Creating a tab, deleting a tab, or changing its contents is not supported through this method. Instead use the general xblock URL (see item.xblock_handler). """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() course_item = modulestore().get_course(course_key) if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if request.method == 'GET': raise NotImplementedError('coming soon') else: if 'tabs' in request.json: return reorder_tabs_handler(course_item, request) elif 'tab_id_locator' in request.json: return edit_tab_handler(course_item, request) else: raise NotImplementedError('Creating or changing tab content is not supported.') elif request.method == 'GET': # assume html # get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs # present in the same order they are displayed in LMS tabs_to_render = [] for tab in CourseTabList.iterate_displayable_cms( course_item, settings, ): if isinstance(tab, StaticTab): # static tab needs its locator information to render itself as an xmodule static_tab_loc = course_key.make_usage_key('static_tab', tab.url_slug) tab.locator = static_tab_loc tabs_to_render.append(tab) return render_to_response('edit-tabs.html', { 'context_course': course_item, 'tabs_to_render': tabs_to_render, 'lms_link': get_lms_link_for_item(course_item.location), }) else: return HttpResponseNotFound()
def export_handler(request, course_key_string): """ The restful handler for exporting a course. GET html: return html page for import page json: not supported POST Start a Celery task to export the course The Studio UI uses a POST request to start the export asynchronously, with a link appearing on the page once it's ready. """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() if isinstance(course_key, LibraryLocator): courselike_module = modulestore().get_library(course_key) context = { 'context_library': courselike_module, 'courselike_home_url': reverse_library_url("library_handler", course_key), 'library': True } else: courselike_module = modulestore().get_course(course_key) if courselike_module is None: raise Http404 context = { 'context_course': courselike_module, 'courselike_home_url': reverse_course_url("course_handler", course_key), 'library': False } context['status_url'] = reverse_course_url('export_status_handler', course_key) # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header. requested_format = request.GET.get( '_accept', request.META.get('HTTP_ACCEPT', 'text/html')) if request.method == 'POST': export_olx.delay(request.user.id, course_key_string, request.LANGUAGE_CODE) return JsonResponse({'ExportStatus': 1}) elif 'text/html' in requested_format: return render_to_response('export.html', context) else: # Only HTML request format is supported (no JSON). return HttpResponse(status=406)
def _wrapper_view(self, request, course_id, *args, **kwargs): """ Checks for course author access for the given course by the requesting user. Calls the view function if has access, otherwise raises a 403. """ course_key = CourseKey.from_string(course_id) if not has_course_author_access(request.user, course_key): raise DeveloperErrorViewMixin.api_error( status_code=status.HTTP_403_FORBIDDEN, developer_message='The requesting user does not have course author permissions.', error_code='user_permissions', ) return view(self, request, course_key, *args, **kwargs)
def entrance_exam(request, course_key_string): """ The restful handler for entrance exams. It allows retrieval of all the assets (as an HTML page), as well as uploading new assets, deleting assets, and changing the "locked" state of an asset. GET Retrieves the entrance exam module (metadata) for the specified course POST Adds an entrance exam module to the specified course. DELETE Removes the entrance exam from the course """ course_key = CourseKey.from_string(course_key_string) # Deny access if the user is valid, but they lack the proper object access privileges if not has_course_author_access(request.user, course_key): return HttpResponse(status=403) # Retrieve the entrance exam module for the specified course (returns 404 if none found) if request.method == 'GET': return _get_entrance_exam(request, course_key) # Create a new entrance exam for the specified course (returns 201 if created) elif request.method == 'POST': response_format = request.REQUEST.get('format', 'html') http_accept = request.META.get('http_accept') if response_format == 'json' or 'application/json' in http_accept: ee_min_score = request.POST.get('entrance_exam_minimum_score_pct', None) # if request contains empty value or none then save the default one. entrance_exam_minimum_score_pct = _get_default_entrance_exam_minimum_pct( ) if ee_min_score != '' and ee_min_score is not None: entrance_exam_minimum_score_pct = float(ee_min_score) return create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct) return HttpResponse(status=400) # Remove the entrance exam module for the specified course (returns 204 regardless of existence) elif request.method == 'DELETE': return delete_entrance_exam(request, course_key) # No other HTTP verbs/methods are supported at this time else: return HttpResponse(status=405)
def export_handler(request, course_key_string): """ The restful handler for exporting a course. GET html: return html page for import page json: not supported POST Start a Celery task to export the course The Studio UI uses a POST request to start the export asynchronously, with a link appearing on the page once it's ready. """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() if isinstance(course_key, LibraryLocator): courselike_module = modulestore().get_library(course_key) context = { 'context_library': courselike_module, 'courselike_home_url': reverse_library_url("library_handler", course_key), 'library': True } else: courselike_module = modulestore().get_course(course_key) if courselike_module is None: raise Http404 context = { 'context_course': courselike_module, 'courselike_home_url': reverse_course_url("course_handler", course_key), 'library': False } context['status_url'] = reverse_course_url('export_status_handler', course_key) # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header. requested_format = request.GET.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html')) if request.method == 'POST': export_olx.delay(request.user.id, course_key_string, request.LANGUAGE_CODE) return JsonResponse({'ExportStatus': 1}) elif 'text/html' in requested_format: return render_to_response('export.html', context) else: # Only HTML request format is supported (no JSON). return HttpResponse(status=406)
def checklists_handler(request, course_key_string=None): ''' The restful handler for course checklists. It allows retrieval of the checklists (as an HTML page). GET html: return an html page which will show course checklists. Note that only the checklists container is returned and that the actual data is determined with a client-side request. ''' course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key) return render_to_response('checklists.html', { 'language_code': request.LANGUAGE_CODE, 'context_course': course_module, })
def _get_item(request, data): """ Obtains from 'data' the locator for an item. Next, gets that item from the modulestore (allowing any errors to raise up). Finally, verifies that the user has access to the item. Returns the item. """ usage_key = UsageKey.from_string(data.get('locator')) # This is placed before has_course_author_access() to validate the location, # because has_course_author_access() raises r if location is invalid. item = modulestore().get_item(usage_key) # use the item's course_key, because the usage_key might not have the run if not has_course_author_access(request.user, item.location.course_key): raise PermissionDenied() return item
def entrance_exam(request, course_key_string): """ The restful handler for entrance exams. It allows retrieval of all the assets (as an HTML page), as well as uploading new assets, deleting assets, and changing the "locked" state of an asset. GET Retrieves the entrance exam module (metadata) for the specified course POST Adds an entrance exam module to the specified course. DELETE Removes the entrance exam from the course """ course_key = CourseKey.from_string(course_key_string) # Deny access if the user is valid, but they lack the proper object access privileges if not has_course_author_access(request.user, course_key): return HttpResponse(status=403) # Retrieve the entrance exam module for the specified course (returns 404 if none found) if request.method == 'GET': return _get_entrance_exam(request, course_key) # Create a new entrance exam for the specified course (returns 201 if created) elif request.method == 'POST': response_format = request.POST.get('format', 'html') http_accept = request.META.get('http_accept') if response_format == 'json' or 'application/json' in http_accept: ee_min_score = request.POST.get('entrance_exam_minimum_score_pct', None) # if request contains empty value or none then save the default one. entrance_exam_minimum_score_pct = _get_default_entrance_exam_minimum_pct() if ee_min_score != '' and ee_min_score is not None: entrance_exam_minimum_score_pct = float(ee_min_score) return create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct) return HttpResponse(status=400) # Remove the entrance exam module for the specified course (returns 204 regardless of existence) elif request.method == 'DELETE': return delete_entrance_exam(request, course_key) # No other HTTP verbs/methods are supported at this time else: return HttpResponse(status=405)
def import_handler(request, course_key_string): """ The restful handler for importing a course. GET html: return html page for import page json: not supported POST or PUT json: import a course via the .tar.gz file specified in request.FILES """ courselike_key = CourseKey.from_string(course_key_string) library = isinstance(courselike_key, LibraryLocator) if library: successful_url = reverse_library_url('library_handler', courselike_key) context_name = 'context_library' courselike_module = modulestore().get_library(courselike_key) else: successful_url = reverse_course_url('course_handler', courselike_key) context_name = 'context_course' courselike_module = modulestore().get_course(courselike_key) if not has_course_author_access(request.user, courselike_key): raise PermissionDenied() if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if request.method == 'GET': raise NotImplementedError('coming soon') else: return _write_chunk(request, courselike_key) elif request.method == 'GET': # assume html status_url = reverse_course_url("import_status_handler", courselike_key, kwargs={'filename': "fillerName"}) return render_to_response( 'import.html', { context_name: courselike_module, 'successful_import_redirect_url': successful_url, 'import_status_url': status_url, 'library': isinstance(courselike_key, LibraryLocator) }) else: return HttpResponseNotFound()
def export_courses_multiple(user, plugin_class, course_keys, tempdir, outfilename, stream=False, check_author_perms=True): """ Build a tar file from multi-course export and either yield its bytes to stream or yield a finished gzipped tar file. """ if not stream: outfilename += ".gz" tarf = os.path.join(tempdir, outfilename) write_method = "w:" if stream else "w:gz" with tarfile.open(tarf, write_method) as out_tar: for course_key in course_keys: if check_author_perms: if not has_course_author_access(user, course_key): logger.warn('User {} has no access to export {}'.format( user, course_key)) continue try: if stream: for tar_bytes in _course_tar_bytes(user, plugin_class, tempdir, course_key, out_tar): yield tar_bytes else: output_filepath, out_fn = _do_course_export( plugin_class, tempdir, course_key) out_tar.add(output_filepath, out_fn) except exceptions.ExportPluginsCourseExportError: continue if stream: yield _get_tar_end_padding_bytes() shutil.rmtree( tempdir ) # clean up the temp files as they won't be otherwise when streaming else: yield out_tar
def test_rerun_course(self): """ Unit tests for :meth: `contentstore.tasks.rerun_course` """ mongo_course1_id = self.import_and_populate_course() # rerun from mongo into split split_course3_id = CourseLocator( org="edx3", course="split3", run="rerun_test" ) # Mark the action as initiated fields = {'display_name': 'rerun'} CourseRerunState.objects.initiated(mongo_course1_id, split_course3_id, self.user, fields['display_name']) result = rerun_course.delay(unicode(mongo_course1_id), unicode(split_course3_id), self.user.id, json.dumps(fields, cls=EdxJSONEncoder)) self.assertEqual(result.get(), "succeeded") self.assertTrue(has_course_author_access(self.user, split_course3_id), "Didn't grant access") rerun_state = CourseRerunState.objects.find_first(course_key=split_course3_id) self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.SUCCEEDED) # try creating rerunning again to same name and ensure it generates error result = rerun_course.delay(unicode(mongo_course1_id), unicode(split_course3_id), self.user.id) self.assertEqual(result.get(), "duplicate course") # the below will raise an exception if the record doesn't exist CourseRerunState.objects.find_first( course_key=split_course3_id, state=CourseRerunUIStateManager.State.FAILED ) # try to hit the generic exception catch with patch('xmodule.modulestore.split_mongo.mongo_connection.MongoConnection.insert_course_index', Mock(side_effect=Exception)): split_course4_id = CourseLocator(org="edx3", course="split3", run="rerun_fail") fields = {'display_name': 'total failure'} CourseRerunState.objects.initiated(split_course3_id, split_course4_id, self.user, fields['display_name']) result = rerun_course.delay(unicode(split_course3_id), unicode(split_course4_id), self.user.id, json.dumps(fields, cls=EdxJSONEncoder)) self.assertIn("exception: ", result.get()) self.assertIsNone(self.store.get_course(split_course4_id), "Didn't delete course after error") CourseRerunState.objects.find_first( course_key=split_course4_id, state=CourseRerunUIStateManager.State.FAILED )
def _get_item_in_course(request, usage_key): """ Helper method for getting the old location, containing course, item, and lms_link for a given locator. Verifies that the caller has permission to access this item. """ # usage_key's course_key may have an empty run property usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key)) course_key = usage_key.course_key if not has_course_author_access(request.user, course_key): raise PermissionDenied() course = modulestore().get_course(course_key) item = modulestore().get_item(usage_key, depth=1) lms_link = get_lms_link_for_item(item.location) return course, item, lms_link
def import_handler(request, course_key_string): """ The restful handler for the import page. GET html: return html page for import page """ courselike_key = CourseKey.from_string(course_key_string) library = isinstance(courselike_key, LibraryLocator) if library: successful_url = reverse_library_url("library_handler", courselike_key) courselike_module = modulestore().get_library(courselike_key) context_name = "context_library" else: successful_url = reverse_course_url("course_handler", courselike_key) courselike_module = modulestore().get_course(courselike_key) context_name = "context_course" if not has_course_author_access(request.user, courselike_key): raise PermissionDenied() return render_to_response( "import.html", { context_name: courselike_module, "successful_import_redirect_url": successful_url, "import_status_url": reverse("course_import_status_handler", kwargs={ "course_key_string": unicode(courselike_key), "filename": "fillerName" }), "import_url": reverse("course_import_export_handler", kwargs={ "course_key_string": unicode(courselike_key), }), "library": library })
def _get_item_in_course(request, usage_key): """ Helper method for getting the old location, containing course, item, lms_link, and preview_lms_link for a given locator. Verifies that the caller has permission to access this item. """ # usage_key's course_key may have an empty run property usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key)) course_key = usage_key.course_key if not has_course_author_access(request.user, course_key): raise PermissionDenied() course = modulestore().get_course(course_key) item = modulestore().get_item(usage_key, depth=1) lms_link = get_lms_link_for_item(item.location) preview_lms_link = get_lms_link_for_item(item.location, preview=True) return course, item, lms_link, preview_lms_link
def import_handler(request, course_key_string): """ The restful handler for importing a course. GET html: return html page for import page json: not supported POST or PUT json: import a course via the .tar.gz file specified in request.FILES """ courselike_key = CourseKey.from_string(course_key_string) library = isinstance(courselike_key, LibraryLocator) if library: successful_url = reverse_library_url('library_handler', courselike_key) context_name = 'context_library' courselike_module = modulestore().get_library(courselike_key) else: successful_url = reverse_course_url('course_handler', courselike_key) context_name = 'context_course' courselike_module = modulestore().get_course(courselike_key) if not has_course_author_access(request.user, courselike_key): raise PermissionDenied() if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if request.method == 'GET': raise NotImplementedError('coming soon') else: return _write_chunk(request, courselike_key) elif request.method == 'GET': # assume html status_url = reverse_course_url( "import_status_handler", courselike_key, kwargs={'filename': "fillerName"} ) return render_to_response('import.html', { context_name: courselike_module, 'successful_import_redirect_url': successful_url, 'import_status_url': status_url, 'library': isinstance(courselike_key, LibraryLocator) }) else: return HttpResponseNotFound()
def course_team_handler(request, course_key_string=None, email=None): """ The restful handler for course team users. GET html: return html page for managing course team json: return json representation of a particular course team member (email is required). POST or PUT json: modify the permissions for a particular course team member (email is required, as well as role in the payload). DELETE: json: remove a particular course team member from the course team (email is required). """ course_key = CourseKey.from_string(course_key_string) if course_key_string else None if not has_course_author_access(request.user, course_key): raise PermissionDenied() if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): return _course_team_user(request, course_key, email) elif request.method == 'GET': # assume html return _manage_users(request, course_key) else: return HttpResponseNotFound()
def xblock_outline_handler(request, usage_key_string): """ The restful handler for requests for XBlock information about the block and its children. This is used by the course outline in particular to construct the tree representation of a course. """ usage_key = usage_key_with_run(usage_key_string) if not has_course_author_access(request.user, usage_key.course_key): raise PermissionDenied() response_format = request.REQUEST.get('format', 'html') if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): store = modulestore() root_xblock = store.get_item(usage_key) return JsonResponse(create_xblock_info( root_xblock, include_child_info=True, course_outline=True, include_children_predicate=lambda xblock: not xblock.category == 'vertical' )) else: return Http404
def assets_handler(request, course_key_string=None, asset_key_string=None): ''' The restful handler for assets. It allows retrieval of all the assets (as an HTML page), as well as uploading new assets, deleting assets, and changing the 'locked' state of an asset. GET html: return an html page which will show all course assets. Note that only the asset container is returned and that the actual assets are filled in with a client-side request. json: returns a page of assets. The following parameters are supported: page: the desired page of results (defaults to 0) page_size: the number of items per page (defaults to 50) sort: the asset field to sort by (defaults to 'date_added') direction: the sort direction (defaults to 'descending') asset_type: the file type to filter items to (defaults to All) text_search: string to filter results by file name (defaults to '') POST json: create (or update?) an asset. The only updating that can be done is changing the lock state. PUT json: update the locked state of an asset DELETE json: delete an asset ''' course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() response_format = _get_response_format(request) if _request_response_format_is_json(request, response_format): if request.method == 'GET': return _assets_json(request, course_key) asset_key = AssetKey.from_string(asset_key_string) if asset_key_string else None return _update_asset(request, course_key, asset_key) elif request.method == 'GET': # assume html return _asset_index(request, course_key) return HttpResponseNotFound()
def assets_handler(request, course_key_string=None, asset_key_string=None): """ The restful handler for assets. It allows retrieval of all the assets (as an HTML page), as well as uploading new assets, deleting assets, and changing the "locked" state of an asset. GET html: return an html page which will show all course assets. Note that only the asset container is returned and that the actual assets are filled in with a client-side request. json: returns a page of assets. The following parameters are supported: page: the desired page of results (defaults to 0) page_size: the number of items per page (defaults to 50) sort: the asset field to sort by (defaults to "date_added") direction: the sort direction (defaults to "descending") POST json: create (or update?) an asset. The only updating that can be done is changing the lock state. PUT json: update the locked state of an asset DELETE json: delete an asset """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() response_format = request.GET.get('format') or request.POST.get( 'format') or 'html' if response_format == 'json' or 'application/json' in request.META.get( 'HTTP_ACCEPT', 'application/json'): if request.method == 'GET': return _assets_json(request, course_key) else: asset_key = AssetKey.from_string( asset_key_string) if asset_key_string else None return _update_asset(request, course_key, asset_key) elif request.method == 'GET': # assume html return _asset_index(request, course_key) else: return HttpResponseNotFound()
def import_handler(request, course_key_string): """ The restful handler for the import page. GET html: return html page for import page """ courselike_key = CourseKey.from_string(course_key_string) library = isinstance(courselike_key, LibraryLocator) if library: successful_url = reverse_library_url("library_handler", courselike_key) courselike_module = modulestore().get_library(courselike_key) context_name = "context_library" else: successful_url = reverse_course_url("course_handler", courselike_key) courselike_module = modulestore().get_course(courselike_key) context_name = "context_course" if not has_course_author_access(request.user, courselike_key): raise PermissionDenied() return render_to_response("import.html", { context_name: courselike_module, "successful_import_redirect_url": successful_url, "import_status_url": reverse( "course_import_status_handler", kwargs={ "course_key_string": unicode(courselike_key), "filename": "fillerName" } ), "import_url": reverse( "course_import_export_handler", kwargs={ "course_key_string": unicode(courselike_key), } ), "library": library })
def import_status_handler(request, course_key_string, filename=None): """ Returns an integer corresponding to the status of a file import. These are: -X : Import unsuccessful due to some error with X as stage [0-3] 0 : No status info found (import done or upload still in progress) 1 : Unpacking 2 : Verifying 3 : Updating 4 : Import successful """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() # The task status record is authoritative once it's been created args = {u'course_key_string': course_key_string, u'archive_name': filename} name = CourseImportTask.generate_name(args) task_status = UserTaskStatus.objects.filter(name=name) for status_filter in STATUS_FILTERS: task_status = status_filter().filter_queryset(request, task_status, import_status_handler) task_status = task_status.order_by(u'-created').first() if task_status is None: # The task hasn't been initialized yet; did we store info in the session already? try: session_status = request.session["import_status"] status = session_status[course_key_string + filename] except KeyError: status = 0 elif task_status.state == UserTaskStatus.SUCCEEDED: status = 4 elif task_status.state in (UserTaskStatus.FAILED, UserTaskStatus.CANCELED): status = max(-(task_status.completed_steps + 1), -3) else: status = min(task_status.completed_steps + 1, 3) return JsonResponse({"ImportStatus": status})
def import_status_handler(request, course_key_string, filename=None): """ Returns an integer corresponding to the status of a file import. These are: -X : Import unsuccessful due to some error with X as stage [0-3] 0 : No status info found (import done or upload still in progress) 1 : Extracting file 2 : Validating. 3 : Importing to mongo 4 : Import successful """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() try: session_status = request.session["import_status"] status = session_status[course_key_string + filename] except KeyError: status = 0 return JsonResponse({"ImportStatus": status})
def xblock_outline_handler(request, usage_key_string): """ The restful handler for requests for XBlock information about the block and its children. This is used by the course outline in particular to construct the tree representation of a course. """ usage_key = usage_key_with_run(usage_key_string) if not has_course_author_access(request.user, usage_key.course_key): raise PermissionDenied() response_format = request.REQUEST.get('format', 'html') if response_format == 'json' or 'application/json' in request.META.get( 'HTTP_ACCEPT', 'application/json'): store = modulestore() root_xblock = store.get_item(usage_key) return JsonResponse( create_xblock_info(root_xblock, include_child_info=True, course_outline=True, include_children_predicate=lambda xblock: not xblock.category == 'vertical')) else: return Http404
def assets_handler(request, course_key_string=None, asset_key_string=None): """ The restful handler for assets. It allows retrieval of all the assets (as an HTML page), as well as uploading new assets, deleting assets, and changing the "locked" state of an asset. GET html: return an html page which will show all course assets. Note that only the asset container is returned and that the actual assets are filled in with a client-side request. json: returns a page of assets. The following parameters are supported: page: the desired page of results (defaults to 0) page_size: the number of items per page (defaults to 50) sort: the asset field to sort by (defaults to "date_added") direction: the sort direction (defaults to "descending") POST json: create (or update?) an asset. The only updating that can be done is changing the lock state. PUT json: update the locked state of an asset DELETE json: delete an asset """ course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() response_format = request.REQUEST.get('format', 'html') if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if request.method == 'GET': return _assets_json(request, course_key) else: asset_key = AssetKey.from_string(asset_key_string) if asset_key_string else None return _update_asset(request, course_key, asset_key) elif request.method == 'GET': # assume html return _asset_index(request, course_key) else: return HttpResponseNotFound()
def orphan_handler(request, course_key_string): """ View for handling orphan related requests. GET gets all of the current orphans. DELETE removes all orphans (requires is_staff access) An orphan is a block whose category is not in the DETACHED_CATEGORY list, is not the root, and is not reachable from the root via children """ course_usage_key = CourseKey.from_string(course_key_string) if request.method == 'GET': if has_course_author_access(request.user, course_usage_key): return JsonResponse([unicode(item) for item in modulestore().get_orphans(course_usage_key)]) else: raise PermissionDenied() if request.method == 'DELETE': if request.user.is_staff: store = modulestore() items = store.get_orphans(course_usage_key) for itemloc in items: # need to delete all versions store.delete_item(itemloc, request.user.id, revision=ModuleStoreEnum.RevisionOption.all) return JsonResponse({'deleted': [unicode(item) for item in items]}) else: raise PermissionDenied()
def get(self, request, subsection_id): """ Returns subection grade data, override grade data and a history of changes made to a specific users specific subsection grade. Args: subsection_id: String representation of a usage_key, which is an opaque key of a persistant subection grade. user_id: An integer represenation of a user """ try: usage_key = UsageKey.from_string(subsection_id) except InvalidKeyError: raise self.api_error( status_code=status.HTTP_404_NOT_FOUND, developer_message='Invalid UsageKey', error_code='invalid_usage_key' ) if not has_course_author_access(request.user, usage_key.course_key): raise DeveloperErrorViewMixin.api_error( status_code=status.HTTP_403_FORBIDDEN, developer_message='The requesting user does not have course author permissions.', error_code='user_permissions', ) try: user_id = int(request.GET.get('user_id')) except ValueError: raise self.api_error( status_code=status.HTTP_404_NOT_FOUND, developer_message='Invalid UserID', error_code='invalid_user_id' ) try: original_grade = PersistentSubsectionGrade.read_grade(user_id, usage_key) except PersistentSubsectionGrade.DoesNotExist: results = SubsectionGradeResponseSerializer({ 'original_grade': None, 'override': None, 'history': [], 'subsection_id': usage_key, 'user_id': user_id, 'course_id': None, }) return Response(results.data) try: override = original_grade.override history = PersistentSubsectionGradeOverrideHistory.objects.filter(override_id=override.id) except PersistentSubsectionGradeOverride.DoesNotExist: override = None history = [] results = SubsectionGradeResponseSerializer({ 'original_grade': original_grade, 'override': override, 'history': history, 'subsection_id': original_grade.usage_key, 'user_id': original_grade.user_id, 'course_id': original_grade.course_id, }) return Response(results.data)