def test_course_updates_invalid_url(self): """ Tests the error conditions for the invalid course updates URL. """ # Testing the response code by passing slash separated course id whose format is valid but no course # having this id exists. invalid_course_key = '{}_blah_blah_blah'.format(self.course.id) course_updates_url = reverse_course_url('course_info_handler', invalid_course_key) response = self.client.get(course_updates_url) self.assertEqual(response.status_code, 404) # Testing the response code by passing split course id whose format is valid but no course # having this id exists. split_course_key = CourseLocator( org='orgASD', course='course_01213', run='Run_0_hhh_hhh_hhh') course_updates_url_split = reverse_course_url('course_info_handler', split_course_key) response = self.client.get(course_updates_url_split) self.assertEqual(response.status_code, 404) # Testing the response by passing split course id whose format is invalid. invalid_course_id = 'invalid.course.key/{}'.format(split_course_key) course_updates_url_split = reverse_course_url('course_info_handler', invalid_course_id) response = self.client.get(course_updates_url_split) self.assertEqual(response.status_code, 404)
def videos_index_html(course): """ Returns an HTML page to display previous video uploads and allow new ones """ return render_to_response( 'videos_index.html', { 'context_course': course, 'image_upload_url': reverse_course_url('video_images_handler', unicode(course.id)), 'video_handler_url': reverse_course_url('videos_handler', unicode(course.id)), 'encodings_download_url': reverse_course_url('video_encodings_download', unicode(course.id)), 'default_video_image_url': _get_default_video_image_url(), 'previous_uploads': _get_index_videos(course), 'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0), 'video_supported_file_formats': VIDEO_SUPPORTED_FILE_FORMATS.keys(), 'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB, 'video_image_settings': { 'video_image_upload_enabled': WAFFLE_SWITCHES.is_enabled(VIDEO_IMAGE_UPLOAD_ENABLED), 'max_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES'], 'min_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'], 'max_width': settings.VIDEO_IMAGE_MAX_WIDTH, 'max_height': settings.VIDEO_IMAGE_MAX_HEIGHT, 'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS } } )
def setUp(self): """ Sets up the test course. """ super(ImportExportTestCase, self).setUp() self.import_url = reverse_course_url('import_handler', self.course.id) self.export_url = reverse_course_url('export_handler', self.course.id)
def setUp(self): """ Sets up the test course. """ super(ExportTestCase, self).setUp() self.url = reverse_course_url('export_handler', self.course.id) self.status_url = reverse_course_url('export_status_handler', self.course.id)
def test_notifications_handler_get(self): state = CourseRerunUIStateManager.State.FAILED action = CourseRerunUIStateManager.ACTION should_display = True # try when no notification exists notification_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={ 'action_state_id': 1, }) resp = self.client.get(notification_url, HTTP_ACCEPT='application/json') # verify that we get an empty dict out self.assertEquals(resp.status_code, 400) # create a test notification rerun_state = CourseRerunState.objects.update_state(course_key=self.course.id, new_state=state, allow_not_found=True) CourseRerunState.objects.update_should_display(entry_id=rerun_state.id, user=UserFactory(), should_display=should_display) # try to get information on this notification notification_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={ 'action_state_id': rerun_state.id, }) resp = self.client.get(notification_url, HTTP_ACCEPT='application/json') json_response = json.loads(resp.content) self.assertEquals(json_response['state'], state) self.assertEquals(json_response['action'], action) self.assertEquals(json_response['should_display'], should_display)
def test_get_course_list_with_same_course_id(self): """ Test getting courses with same id but with different name case. Then try to delete one of them and check that it is properly deleted and other one is accessible """ # create and log in a non-staff user self.user = UserFactory() request = self.factory.get('/course') request.user = self.user self.client.login(username=self.user.username, password='******') course_location_caps = SlashSeparatedCourseKey('Org', 'COURSE', 'Run') self._create_course_with_access_groups(course_location_caps, self.user) # get courses through iterating all courses courses_list = _accessible_courses_list(request) self.assertEqual(len(courses_list), 1) # get courses by reversing group name formats courses_list_by_groups = _accessible_courses_list_from_groups(request) self.assertEqual(len(courses_list_by_groups), 1) # check both course lists have same courses self.assertEqual(courses_list, courses_list_by_groups) # now create another course with same course_id but different name case course_location_camel = SlashSeparatedCourseKey('Org', 'Course', 'Run') self._create_course_with_access_groups(course_location_camel, self.user) # test that get courses through iterating all courses returns both courses courses_list = _accessible_courses_list(request) self.assertEqual(len(courses_list), 2) # test that get courses by reversing group name formats returns both courses courses_list_by_groups = _accessible_courses_list_from_groups(request) self.assertEqual(len(courses_list_by_groups), 2) # now delete first course (course_location_caps) and check that it is no longer accessible delete_course_and_groups(course_location_caps, commit=True) # test that get courses through iterating all courses now returns one course courses_list = _accessible_courses_list(request) self.assertEqual(len(courses_list), 1) # test that get courses by reversing group name formats also returns one course courses_list_by_groups = _accessible_courses_list_from_groups(request) self.assertEqual(len(courses_list_by_groups), 1) # now check that deleted course is not accessible outline_url = reverse_course_url('course_handler', course_location_caps) response = self.client.get(outline_url, HTTP_ACCEPT='application/json') self.assertEqual(response.status_code, 403) # now check that other course is accessible outline_url = reverse_course_url('course_handler', course_location_camel) response = self.client.get(outline_url, HTTP_ACCEPT='application/json') self.assertEqual(response.status_code, 200)
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 test_get_course_list_with_same_course_id(self): """ Test getting courses with same id but with different name case. Then try to delete one of them and check that it is properly deleted and other one is accessible """ course_location_caps = SlashSeparatedCourseKey("Org", "COURSE", "Run") self._create_course_with_access_groups(course_location_caps, self.user) # get courses through iterating all courses courses_list = _accessible_courses_list(self.request) self.assertEqual(len(courses_list), 1) # get courses by reversing group name formats courses_list_by_groups = _accessible_courses_list_from_groups(self.request) self.assertEqual(len(courses_list_by_groups), 1) # check both course lists have same courses self.assertEqual(courses_list, courses_list_by_groups) # now create another course with same course_id but different name case course_location_camel = SlashSeparatedCourseKey("Org", "Course", "Run") self._create_course_with_access_groups(course_location_camel, self.user) # test that get courses through iterating all courses returns both courses courses_list = _accessible_courses_list(self.request) self.assertEqual(len(courses_list), 2) # test that get courses by reversing group name formats returns both courses courses_list_by_groups = _accessible_courses_list_from_groups(self.request) self.assertEqual(len(courses_list_by_groups), 2) # now delete first course (course_location_caps) and check that it is no longer accessible delete_course_and_groups(course_location_caps, self.user.id) # test that get courses through iterating all courses now returns one course courses_list = _accessible_courses_list(self.request) self.assertEqual(len(courses_list), 1) # test that get courses by reversing group name formats also returns one course courses_list_by_groups = _accessible_courses_list_from_groups(self.request) self.assertEqual(len(courses_list_by_groups), 1) # now check that deleted course is not accessible outline_url = reverse_course_url("course_handler", course_location_caps) response = self.client.get(outline_url, HTTP_ACCEPT="application/json") self.assertEqual(response.status_code, 403) # now check that other course is accessible outline_url = reverse_course_url("course_handler", course_location_camel) response = self.client.get(outline_url, HTTP_ACCEPT="application/json") self.assertEqual(response.status_code, 200)
def videos_index_html(course): """ Returns an HTML page to display previous video uploads and allow new ones """ return render_to_response( "videos_index.html", { "context_course": course, "post_url": reverse_course_url("videos_handler", unicode(course.id)), "encodings_download_url": reverse_course_url("video_encodings_download", unicode(course.id)), "previous_uploads": _get_index_videos(course), "concurrent_upload_limit": settings.VIDEO_UPLOAD_PIPELINE.get("CONCURRENT_UPLOAD_LIMIT", 0), }, )
def group_configurations_list_handler(request, course_key_string): """ A RESTful handler for Group Configurations GET html: return Group Configurations list page (Backbone application) POST json: create new group configuration """ course_key = CourseKey.from_string(course_key_string) course = _get_course_module(course_key, request.user) store = modulestore() if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): group_configuration_url = reverse_course_url('group_configurations_list_handler', course_key) split_test_enabled = SPLIT_TEST_COMPONENT_TYPE in course.advanced_modules return render_to_response('group_configurations.html', { 'context_course': course, 'group_configuration_url': group_configuration_url, 'configurations': [u.to_json() for u in course.user_partitions] if split_test_enabled else None, }) elif "application/json" in request.META.get('HTTP_ACCEPT') and request.method == 'POST': # create a new group configuration for the course try: configuration = GroupConfiguration.parse(request.body) GroupConfiguration.validate(configuration) except GroupConfigurationsValidationError as err: return JsonResponse({"error": err.message}, status=400) if not configuration.get("id"): configuration["id"] = random.randint(100, 10**12) # Assign ids to every group in configuration. for index, group in enumerate(configuration.get('groups', [])): group["id"] = index course.user_partitions.append(UserPartition.from_json(configuration)) store.update_item(course, request.user.id) response = JsonResponse(configuration, status=201) response["Location"] = reverse_course_url( 'group_configurations_detail_handler', course.id, kwargs={'group_configuration_id': configuration["id"]} ) return response else: return HttpResponse(status=406)
def test_delete_asset_with_invalid_asset(self): """ Tests the sad path :( """ test_url = reverse_course_url( "assets_handler", self.course.id, kwargs={"asset_key_string": unicode("/c4x/edX/toy/asset/invalid.pdf")} ) resp = self.client.delete(test_url, HTTP_ACCEPT="application/json") self.assertEquals(resp.status_code, 404)
def test_start_date_on_page(self): """ Verify that the course start date is included on the course outline page. """ def _get_release_date(response): """Return the release date from the course page""" parsed_html = lxml.html.fromstring(response.content) return parsed_html.find_class('course-status')[0].find_class('status-release-value')[0].text_content() def _assert_settings_link_present(response): """ Asserts there's a course settings link on the course page by the course release date. """ parsed_html = lxml.html.fromstring(response.content) settings_link = parsed_html.find_class('course-status')[0].find_class('action-edit')[0].find('a') self.assertIsNotNone(settings_link) self.assertEqual(settings_link.get('href'), reverse_course_url('settings_handler', self.course.id)) outline_url = reverse_course_url('course_handler', self.course.id) response = self.client.get(outline_url, {}, HTTP_ACCEPT='text/html') # A course with the default release date should display as "Unscheduled" self.assertEqual(_get_release_date(response), 'Unscheduled') _assert_settings_link_present(response) self.course.start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc) modulestore().update_item(self.course, ModuleStoreEnum.UserID.test) response = self.client.get(outline_url, {}, HTTP_ACCEPT='text/html') self.assertEqual(_get_release_date(response), get_default_time_display(self.course.start)) _assert_settings_link_present(response)
def test_certificate_activation_success(self): """ Activate and Deactivate the course certificate """ test_url = reverse_course_url("certificates.certificate_activation_handler", self.course.id) self._add_course_certificates(count=1, signatory_count=2) is_active = True for i in range(2): if i == 1: is_active = not is_active response = self.client.post( test_url, data=json.dumps({"is_active": is_active}), content_type="application/json", HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEquals(response.status_code, 200) course = self.store.get_course(self.course.id) certificates = course.certificates["certificates"] self.assertEqual(certificates[0].get("is_active"), is_active) cert_event_type = "activated" if is_active else "deactivated" self.assert_event_emitted( ".".join(["edx.certificate.configuration", cert_event_type]), course_id=unicode(self.course.id) )
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: root_name = LIBRARY_ROOT successful_url = reverse_library_url('library_handler', courselike_key) context_name = 'context_library' courselike_module = modulestore().get_library(courselike_key) import_func = import_library_from_xml else: root_name = COURSE_ROOT successful_url = reverse_course_url('course_handler', courselike_key) context_name = 'context_course' courselike_module = modulestore().get_course(courselike_key) import_func = import_course_from_xml return _import_handler( request, courselike_key, root_name, successful_url, context_name, courselike_module, import_func )
def setUp(self): super(ImportEntranceExamTestCase, self).setUp() self.url = reverse_course_url('import_handler', self.course.id) self.content_dir = path(tempfile.mkdtemp()) self.addCleanup(shutil.rmtree, self.content_dir) # Create tar test file ----------------------------------------------- # OK course with entrance exam section: entrance_exam_dir = tempfile.mkdtemp(dir=self.content_dir) # test course being deeper down than top of tar file embedded_exam_dir = os.path.join(entrance_exam_dir, "grandparent", "parent") os.makedirs(os.path.join(embedded_exam_dir, "course")) os.makedirs(os.path.join(embedded_exam_dir, "chapter")) with open(os.path.join(embedded_exam_dir, "course.xml"), "w+") as f: f.write('<course url_name="2013_Spring" org="EDx" course="0.00x"/>') with open(os.path.join(embedded_exam_dir, "course", "2013_Spring.xml"), "w+") as f: f.write( '<course ' 'entrance_exam_enabled="true" entrance_exam_id="xyz" entrance_exam_minimum_score_pct="0.7">' '<chapter url_name="2015_chapter_entrance_exam"/></course>' ) with open(os.path.join(embedded_exam_dir, "chapter", "2015_chapter_entrance_exam.xml"), "w+") as f: f.write('<chapter display_name="Entrance Exam" in_entrance_exam="true" is_entrance_exam="true"></chapter>') self.entrance_exam_tar = os.path.join(self.content_dir, "entrance_exam.tar.gz") with tarfile.open(self.entrance_exam_tar, "w:gz") as gtar: gtar.add(entrance_exam_dir)
def test_unsafe_tar(self): """ Check that safety measure work. This includes: 'tarbombs' which include files or symlinks with paths outside or directly in the working directory, 'special files' (character device, block device or FIFOs), all raise exceptions/400s. """ def try_tar(tarpath): with open(tarpath) as tar: args = {"name": tarpath, "course-data": [tar]} resp = self.client.post(self.url, args) self.assertEquals(resp.status_code, 400) self.assertTrue("SuspiciousFileOperation" in resp.content) try_tar(self._fifo_tar()) try_tar(self._symlink_tar()) try_tar(self._outside_tar()) try_tar(self._outside_tar2()) # Check that `import_status` returns the appropriate stage (i.e., # either 3, indicating all previous steps are completed, or 0, # indicating no upload in progress) resp_status = self.client.get( reverse_course_url( 'import_status_handler', self.course.id, kwargs={'filename': os.path.split(self.good_tar)[1]} ) ) import_status = json.loads(resp_status.content)["ImportStatus"] self.assertIn(import_status, (0, 3))
def test_json_responses(self): outline_url = reverse_course_url('course_handler', self.course.id) chapter = ItemFactory.create(parent_location=self.course.location, category='chapter', display_name="Week 1") lesson = ItemFactory.create(parent_location=chapter.location, category='sequential', display_name="Lesson 1") subsection = ItemFactory.create(parent_location=lesson.location, category='vertical', display_name='Subsection 1') ItemFactory.create(parent_location=subsection.location, category="video", display_name="My Video") resp = self.client.get(outline_url, HTTP_ACCEPT='application/json') json_response = json.loads(resp.content) # First spot check some values in the root response self.assertEqual(json_response['category'], 'course') self.assertEqual(json_response['id'], 'location:MITx+999+Robot_Super_Course+course+Robot_Super_Course') self.assertEqual(json_response['display_name'], 'Robot Super Course') self.assertTrue(json_response['is_container']) self.assertFalse(json_response['is_draft']) # Now verify the first child children = json_response['children'] self.assertTrue(len(children) > 0) first_child_response = children[0] self.assertEqual(first_child_response['category'], 'chapter') self.assertEqual(first_child_response['id'], 'location:MITx+999+Robot_Super_Course+chapter+Week_1') self.assertEqual(first_child_response['display_name'], 'Week 1') self.assertTrue(first_child_response['is_container']) self.assertFalse(first_child_response['is_draft']) self.assertTrue(len(first_child_response['children']) > 0) # Finally, validate the entire response for consistency self.assert_correct_json_response(json_response)
def test_json_responses(self): """ Verify the JSON responses returned for the course. """ outline_url = reverse_course_url('course_handler', self.course.id) resp = self.client.get(outline_url, HTTP_ACCEPT='application/json') json_response = json.loads(resp.content) # First spot check some values in the root response self.assertEqual(json_response['category'], 'course') self.assertEqual(json_response['id'], unicode(self.course.location)) self.assertEqual(json_response['display_name'], self.course.display_name) self.assertTrue(json_response['published']) self.assertIsNone(json_response['visibility_state']) # Now verify the first child children = json_response['child_info']['children'] self.assertTrue(len(children) > 0) first_child_response = children[0] self.assertEqual(first_child_response['category'], 'chapter') self.assertEqual(first_child_response['id'], unicode(self.chapter.location)) self.assertEqual(first_child_response['display_name'], 'Week 1') self.assertTrue(json_response['published']) self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled) self.assertTrue(len(first_child_response['child_info']['children']) > 0) # Finally, validate the entire response for consistency self.assert_correct_json_response(json_response)
def videos_index_html(course): """ Returns an HTML page to display previous video uploads and allow new ones """ return render_to_response( "videos_index.html", { "context_course": course, "video_handler_url": reverse_course_url("videos_handler", unicode(course.id)), "encodings_download_url": reverse_course_url("video_encodings_download", unicode(course.id)), "previous_uploads": _get_index_videos(course), "concurrent_upload_limit": settings.VIDEO_UPLOAD_PIPELINE.get("CONCURRENT_UPLOAD_LIMIT", 0), "video_supported_file_formats": VIDEO_SUPPORTED_FILE_FORMATS.keys(), "video_upload_max_file_size": VIDEO_UPLOAD_MAX_FILE_SIZE_GB } )
def setUp(self): """ Setup test course, user, and url. """ super(TestExportGit, self).setUp() self.course_module = modulestore().get_course(self.course.id) self.test_url = reverse_course_url('export_git', self.course.id)
def test_certificate_activation_success(self, signatory_path): """ Activate and Deactivate the course certificate """ test_url = reverse_course_url('certificates.certificate_activation_handler', self.course.id) self._add_course_certificates(count=1, signatory_count=2, asset_path_format=signatory_path) is_active = True for i in range(2): if i == 1: is_active = not is_active response = self.client.post( test_url, data=json.dumps({"is_active": is_active}), content_type="application/json", HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest" ) self.assertEquals(response.status_code, 200) course = self.store.get_course(self.course.id) certificates = course.certificates['certificates'] self.assertEqual(certificates[0].get('is_active'), is_active) cert_event_type = 'activated' if is_active else 'deactivated' self.assert_event_emitted( '.'.join(['edx.certificate.configuration', cert_event_type]), course_id=unicode(self.course.id), )
def setUp(self): """Create initial data.""" super(Basetranscripts, self).setUp() self.location = self.course.id.make_usage_key('course', self.course.id.run) self.captions_url = reverse_course_url('utility_captions_handler', self.course.id) self.unicode_locator = unicode(self.location) # Add video module data = { # 'parent_locator': self.location.to_deprecated_string(), 'parent_locator': self.unicode_locator, 'category': 'video', 'type': 'video' } resp = self.client.ajax_post('http://testserver/xblock/', data) videos = get_videos(self.course) self.item_location = self._get_locator(resp) self.item_location_string = str(self.item_location) self.assertEqual(resp.status_code, 200) self.item = modulestore().get_item(self.item_location) # hI10vDNYz4M - valid Youtube ID with transcripts. # JMD_ifUUfsU, AKqURZnYqpk, DYpADpL7jAY - valid Youtube IDs without transcripts. self.item.data = '<video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY" />' modulestore().update_item(self.item, self.user.id) self.item = modulestore().get_item(self.item_location) # Remove all transcripts for current module. self.clear_subs_content()
def test_delete_asset(self): """ Tests the happy path :) """ test_url = reverse_course_url( "assets_handler", self.course.id, kwargs={"asset_key_string": unicode(self.uploaded_url)} ) resp = self.client.delete(test_url, HTTP_ACCEPT="application/json") self.assertEquals(resp.status_code, 204)
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 test_output_non_course_author(self): """ Verify that users who aren't authors of the course are unable to see the output of export tasks """ client, _ = self.create_non_staff_authed_user_client() resp = client.get(reverse_course_url('export_output_handler', self.course.id)) self.assertEqual(resp.status_code, 403)
def test_delete_image_type_asset(self): """ Tests deletion of image type asset """ image_asset = self.get_sample_asset(self.asset_name, asset_type="image") thumbnail_image_asset = self.get_sample_asset('delete_test_thumbnail', asset_type="image") # upload image response = self.client.post(self.url, {"name": "delete_image_test", "file": image_asset}) self.assertEquals(response.status_code, 200) uploaded_image_url = json.loads(response.content)['asset']['url'] # upload image thumbnail response = self.client.post(self.url, {"name": "delete_image_thumb_test", "file": thumbnail_image_asset}) self.assertEquals(response.status_code, 200) thumbnail_url = json.loads(response.content)['asset']['url'] thumbnail_location = StaticContent.get_location_from_path(thumbnail_url) image_asset_location = AssetLocation.from_deprecated_string(uploaded_image_url) content = contentstore().find(image_asset_location) content.thumbnail_location = thumbnail_location contentstore().save(content) with mock.patch('opaque_keys.edx.locator.CourseLocator.make_asset_key') as mock_asset_key: mock_asset_key.return_value = thumbnail_location test_url = reverse_course_url( 'assets_handler', self.course.id, kwargs={'asset_key_string': unicode(uploaded_image_url)}) resp = self.client.delete(test_url, HTTP_ACCEPT="application/json") self.assertEquals(resp.status_code, 204)
def setUp(self): super(ImportTestCase, self).setUp() self.url = reverse_course_url('import_handler', self.course.id) self.content_dir = path(tempfile.mkdtemp()) def touch(name): """ Equivalent to shell's 'touch'""" with file(name, 'a'): os.utime(name, None) # Create tar test files ----------------------------------------------- # OK course: good_dir = tempfile.mkdtemp(dir=self.content_dir) os.makedirs(os.path.join(good_dir, "course")) with open(os.path.join(good_dir, "course.xml"), "w+") as f: f.write('<course url_name="2013_Spring" org="EDx" course="0.00x"/>') with open(os.path.join(good_dir, "course", "2013_Spring.xml"), "w+") as f: f.write('<course></course>') self.good_tar = os.path.join(self.content_dir, "good.tar.gz") with tarfile.open(self.good_tar, "w:gz") as gtar: gtar.add(good_dir) # Bad course (no 'course.xml' file): bad_dir = tempfile.mkdtemp(dir=self.content_dir) touch(os.path.join(bad_dir, "bad.xml")) self.bad_tar = os.path.join(self.content_dir, "bad.tar.gz") with tarfile.open(self.bad_tar, "w:gz") as btar: btar.add(bad_dir) self.unsafe_common_dir = path(tempfile.mkdtemp(dir=self.content_dir))
def test_manage_library_users(self): """ Simple test that the Library "User Access" view works. Also tests that we can use the REST API to assign a user to a library. """ library = LibraryFactory.create() extra_user, _ = self.create_non_staff_user() manage_users_url = reverse_library_url('manage_library_users', unicode(library.location.library_key)) response = self.client.get(manage_users_url) self.assertEqual(response.status_code, 200) # extra_user has not been assigned to the library so should not show up in the list: self.assertNotIn(extra_user.username, response.content) # Now add extra_user to the library: user_details_url = reverse_course_url( 'course_team_handler', library.location.library_key, kwargs={'email': extra_user.email} ) edit_response = self.client.ajax_post(user_details_url, {"role": LibraryUserRole.ROLE}) self.assertIn(edit_response.status_code, (200, 204)) # Now extra_user should apear in the list: response = self.client.get(manage_users_url) self.assertEqual(response.status_code, 200) self.assertIn(extra_user.username, response.content)
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 test_json_responses(self): outline_url = reverse_course_url('course_handler', self.course.id) chapter = ItemFactory.create(parent_location=self.course.location, category='chapter', display_name="Week 1") lesson = ItemFactory.create(parent_location=chapter.location, category='sequential', display_name="Lesson 1") subsection = ItemFactory.create( parent_location=lesson.location, category='vertical', display_name='Subsection 1' ) ItemFactory.create(parent_location=subsection.location, category="video", display_name="My Video") resp = self.client.get(outline_url, HTTP_ACCEPT='application/json') json_response = json.loads(resp.content) # First spot check some values in the root response self.assertEqual(json_response['category'], 'course') self.assertEqual(json_response['id'], unicode(self.course.location)) self.assertEqual(json_response['display_name'], self.course.display_name) self.assertTrue(json_response['published']) self.assertIsNone(json_response['visibility_state']) # Now verify the first child children = json_response['child_info']['children'] self.assertTrue(len(children) > 0) first_child_response = children[0] self.assertEqual(first_child_response['category'], 'chapter') self.assertEqual(first_child_response['id'], unicode(chapter.location)) self.assertEqual(first_child_response['display_name'], 'Week 1') self.assertTrue(json_response['published']) self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled) self.assertTrue(len(first_child_response['child_info']['children']) > 0) # Finally, validate the entire response for consistency self.assert_correct_json_response(json_response)
def test_no_coursexml(self): """ Check that the response for a tar.gz import without a course.xml is correct. """ with open(self.bad_tar) as btar: resp = self.client.post( self.url, { "name": self.bad_tar, "course-data": [btar] }) self.assertEquals(resp.status_code, 415) # Check that `import_status` returns the appropriate stage (i.e., the # stage at which import failed). resp_status = self.client.get( reverse_course_url( 'import_status_handler', self.course.id, kwargs={'filename': os.path.split(self.bad_tar)[1]} ) ) self.assertEquals(json.loads(resp_status.content)["ImportStatus"], -2)
def setUp(self): super(ImportEntranceExamTestCase, self).setUp() self.url = reverse_course_url('import_handler', self.course.id) self.content_dir = path(tempfile.mkdtemp()) self.addCleanup(shutil.rmtree, self.content_dir) # Create tar test file ----------------------------------------------- # OK course with entrance exam section: entrance_exam_dir = tempfile.mkdtemp(dir=self.content_dir) # test course being deeper down than top of tar file embedded_exam_dir = os.path.join(entrance_exam_dir, "grandparent", "parent") os.makedirs(os.path.join(embedded_exam_dir, "course")) os.makedirs(os.path.join(embedded_exam_dir, "chapter")) with open(os.path.join(embedded_exam_dir, "course.xml"), "w+") as f: f.write( '<course url_name="2013_Spring" org="EDx" course="0.00x"/>') with open(os.path.join(embedded_exam_dir, "course", "2013_Spring.xml"), "w+") as f: f.write( '<course ' 'entrance_exam_enabled="true" entrance_exam_id="xyz" entrance_exam_minimum_score_pct="0.7">' '<chapter url_name="2015_chapter_entrance_exam"/></course>') with open( os.path.join(embedded_exam_dir, "chapter", "2015_chapter_entrance_exam.xml"), "w+") as f: f.write( '<chapter display_name="Entrance Exam" in_entrance_exam="true" is_entrance_exam="true"></chapter>' ) self.entrance_exam_tar = os.path.join(self.content_dir, "entrance_exam.tar.gz") with tarfile.open(self.entrance_exam_tar, "w:gz") as gtar: gtar.add(entrance_exam_dir)
def test_unsafe_tar(self): """ Check that safety measure work. This includes: 'tarbombs' which include files or symlinks with paths outside or directly in the working directory, 'special files' (character device, block device or FIFOs), all raise exceptions/400s. """ def try_tar(tarpath): """ Attempt to tar an unacceptable file """ with open(tarpath) as tar: args = {"name": tarpath, "course-data": [tar]} resp = self.client.post(self.url, args) self.assertEquals(resp.status_code, 400) self.assertTrue("SuspiciousFileOperation" in resp.content) try_tar(self._fifo_tar()) try_tar(self._symlink_tar()) try_tar(self._outside_tar()) try_tar(self._outside_tar2()) # Check that `import_status` returns the appropriate stage (i.e., # either 3, indicating all previous steps are completed, or 0, # indicating no upload in progress) resp_status = self.client.get( reverse_course_url( 'import_status_handler', self.course.id, kwargs={'filename': os.path.split(self.good_tar)[1]} ) ) import_status = json.loads(resp_status.content)["ImportStatus"] self.assertIn(import_status, (0, 3))
def test_certificate_activation_failure(self, signatory_path): """ Certificate activation should fail when user has not read access to course then permission denied exception should raised. """ test_url = reverse_course_url('certificate_activation_handler', self.course.id) test_user_client, test_user = self.create_non_staff_authed_user_client( ) CourseEnrollment.enroll(test_user, self.course.id) self._add_course_certificates(count=1, signatory_count=2, asset_path_format=signatory_path) response = test_user_client.post( test_url, data=json.dumps({"is_active": True}), content_type="application/json", HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 403) course = self.store.get_course(self.course.id) certificates = course.certificates['certificates'] self.assertEqual(certificates[0].get('is_active'), False)
def test_start_date_on_page(self): """ Verify that the course start date is included on the course outline page. """ def _get_release_date(response): """Return the release date from the course page""" parsed_html = lxml.html.fromstring(response.content) return parsed_html.find_class('course-status')[0].find_class( 'status-release-value')[0].text_content() def _assert_settings_link_present(response): """ Asserts there's a course settings link on the course page by the course release date. """ parsed_html = lxml.html.fromstring(response.content) settings_link = parsed_html.find_class( 'course-status')[0].find_class('action-edit')[0].find('a') self.assertIsNotNone(settings_link) self.assertEqual( settings_link.get('href'), reverse_course_url('settings_handler', self.course.id)) outline_url = reverse_course_url('course_handler', self.course.id) response = self.client.get(outline_url, {}, HTTP_ACCEPT='text/html') # A course with the default release date should display as "Unscheduled" self.assertEqual(_get_release_date(response), 'Unscheduled') _assert_settings_link_present(response) self.course.start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc) modulestore().update_item(self.course, ModuleStoreEnum.UserID.test) response = self.client.get(outline_url, {}, HTTP_ACCEPT='text/html') self.assertEqual(_get_release_date(response), get_default_time_display(self.course.start)) _assert_settings_link_present(response)
def _url(self): """ Return url for the handler. """ return reverse_course_url('certificates_list_handler', self.course.id)
def setUp(self): "Set the URL for tests" super(TextbookIndexTestCase, self).setUp() self.url = reverse_course_url('textbooks_list_handler', self.course.id)
def export_status_handler(request, course_key_string): """ Returns an integer corresponding to the status of a file export. These are: -X : Export unsuccessful due to some error with X as stage [0-3] 0 : No status info found (export done or task not yet created) 1 : Exporting 2 : Compressing 3 : Export successful If the export was successful, a URL for the generated .tar.gz file is also returned. """ 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 task_status = _latest_task_status(request, course_key_string, export_status_handler) output_url = None error = None 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["export_status"] status = session_status[course_key_string] except KeyError: status = 0 elif task_status.state == UserTaskStatus.SUCCEEDED: status = 3 artifact = UserTaskArtifact.objects.get(status=task_status, name='Output') if isinstance(artifact.file.storage, FileSystemStorage): output_url = reverse_course_url('export_output_handler', course_key) elif isinstance(artifact.file.storage, S3BotoStorage): filename = os.path.basename(artifact.file.name) disposition = u'attachment; filename="{}"'.format(filename) output_url = artifact.file.storage.url( artifact.file.name, response_headers={ 'response-content-disposition': disposition, 'response-content-encoding': 'application/octet-stream', 'response-content-type': 'application/x-tgz' }) else: output_url = artifact.file.storage.url(artifact.file.name) elif task_status.state in (UserTaskStatus.FAILED, UserTaskStatus.CANCELED): status = max(-(task_status.completed_steps + 1), -2) errors = UserTaskArtifact.objects.filter(status=task_status, name='Error') if errors: error = errors[0].text try: error = json.loads(error) except ValueError: # Wasn't JSON, just use the value as a string pass else: status = min(task_status.completed_steps + 1, 2) response = {"ExportStatus": status} if output_url: response['ExportOutput'] = output_url elif error: response['ExportError'] = error return JsonResponse(response)
def test_course_update(self): """Go through each interface and ensure it works.""" def get_response(content, date): """ Helper method for making call to server and returning response. Does not supply a provided_id. """ payload = {'content': content, 'date': date} url = self.create_update_url() resp = self.client.ajax_post(url, payload) self.assertContains(resp, '', status_code=200) return json.loads(resp.content) resp = self.client.get_html( reverse_course_url('course_info_handler', self.course.id)) self.assertContains(resp, 'Course Updates', status_code=200) init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">' content = init_content + '</iframe>' payload = get_response(content, 'January 8, 2013') self.assertHTMLEqual(payload['content'], content) first_update_url = self.create_update_url(provided_id=payload['id']) content += '<div>div <p>p<br/></p></div>' payload['content'] = content # POST requests were coming in w/ these header values causing an error; so, repro error here resp = self.client.ajax_post(first_update_url, payload, HTTP_X_HTTP_METHOD_OVERRIDE="PUT", REQUEST_METHOD="POST") self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div") # refetch using provided id refetched = self.client.get_json(first_update_url) self.assertHTMLEqual(content, json.loads(refetched.content)['content'], "get w/ provided id") # now put in an evil update content = '<ol/>' payload = get_response(content, 'January 11, 2013') self.assertHTMLEqual(content, payload['content'], "self closing ol") course_update_url = self.create_update_url() resp = self.client.get_json(course_update_url) payload = json.loads(resp.content) self.assertTrue(len(payload) == 2) # try json w/o required fields self.assertContains(self.client.ajax_post(course_update_url, {'garbage': 1}), 'Failed to save', status_code=400) # test an update with text in the tail of the header content = 'outside <strong>inside</strong> after' payload = get_response(content, 'June 22, 2000') self.assertHTMLEqual(content, payload['content'], "text outside tag") # now try to update a non-existent update content = 'blah blah' payload = {'content': content, 'date': 'January 21, 2013'} self.assertContains(self.client.ajax_post(course_update_url + '9', payload), 'Failed to save', status_code=400) # update w/ malformed html content = '<garbage tag No closing brace to force <span>error</span>' payload = {'content': content, 'date': 'January 11, 2013'} self.assertContains(self.client.ajax_post(course_update_url, payload), '<garbage') # set to valid html which would break an xml parser content = "<p><br><br></p>" payload = get_response(content, 'January 11, 2013') self.assertHTMLEqual(content, payload['content']) # now try to delete a non-existent update self.assertContains(self.client.delete(course_update_url + '19'), "delete", status_code=400) # now delete a real update content = 'blah blah' payload = get_response(content, 'January 28, 2013') this_id = payload['id'] self.assertHTMLEqual(content, payload['content'], "single iframe") # first count the entries resp = self.client.get_json(course_update_url) payload = json.loads(resp.content) before_delete = len(payload) url = self.create_update_url(provided_id=this_id) resp = self.client.delete(url) payload = json.loads(resp.content) self.assertTrue(len(payload) == before_delete - 1)
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_access # and remove_user work user = users.pop() group.add_users(user) user_by_role[role].append(user) self.assertTrue(has_course_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 = SlashSeparatedCourseKey('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(has_course_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(has_course_access(user, self.course_key), "{} remove didn't work".format(user))
def _url(self): """ Return url for the handler. """ return reverse_course_url('group_configurations_list_handler', self.course.id)
def create_update_url(self, provided_id=None, course_key=None): if course_key is None: course_key = self.course.id kwargs = {'provided_id': str(provided_id)} if provided_id else None return reverse_course_url('course_info_update_handler', course_key, kwargs=kwargs)
def _write_chunk(request, courselike_key): """ Write the OLX file data chunk from the given request to the local filesystem. """ # Upload .tar.gz to local filesystem for one-server installations not using S3 or Swift data_root = path(settings.GITHUB_REPO_ROOT) subdir = base64.urlsafe_b64encode(repr(courselike_key)) course_dir = data_root / subdir filename = request.FILES['course-data'].name courselike_string = text_type(courselike_key) + filename # Do everything in a try-except block to make sure everything is properly cleaned up. try: # Use sessions to keep info about import progress _save_request_status(request, courselike_string, 0) if not filename.endswith('.tar.gz'): _save_request_status(request, courselike_string, -1) return JsonResponse( { 'ErrMsg': _('We only support uploading a .tar.gz file.'), 'Stage': -1 }, status=415) temp_filepath = course_dir / filename if not course_dir.isdir(): # pylint: disable=no-value-for-parameter os.mkdir(course_dir) logging.debug('importing course to {0}'.format(temp_filepath)) # Get upload chunks byte ranges try: matches = CONTENT_RE.search(request.META["HTTP_CONTENT_RANGE"]) content_range = matches.groupdict() except KeyError: # Single chunk # no Content-Range header, so make one that will work content_range = {'start': 0, 'stop': 1, 'end': 2} # stream out the uploaded files in chunks to disk if int(content_range['start']) == 0: mode = "wb+" else: mode = "ab+" size = os.path.getsize(temp_filepath) # Check to make sure we haven't missed a chunk # This shouldn't happen, even if different instances are handling # the same session, but it's always better to catch errors earlier. if size < int(content_range['start']): _save_request_status(request, courselike_string, -1) log.warning( "Reported range %s does not match size downloaded so far %s", content_range['start'], size) return JsonResponse( { 'ErrMsg': _('File upload corrupted. Please try again'), 'Stage': -1 }, status=409) # The last request sometimes comes twice. This happens because # nginx sends a 499 error code when the response takes too long. elif size > int(content_range['stop']) and size == int( content_range['end']): return JsonResponse({'ImportStatus': 1}) with open(temp_filepath, mode) as temp_file: for chunk in request.FILES['course-data'].chunks(): temp_file.write(chunk) size = os.path.getsize(temp_filepath) if int(content_range['stop']) != int(content_range['end']) - 1: # More chunks coming return JsonResponse({ "files": [{ "name": filename, "size": size, "deleteUrl": "", "deleteType": "", "url": reverse_course_url('import_handler', courselike_key), "thumbnailUrl": "" }] }) 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) import_olx.delay(request.user.id, text_type(courselike_key), storage_path, filename, request.LANGUAGE_CODE) # Send errors to client with stage at which error occurred. except Exception as exception: # pylint: disable=broad-except _save_request_status(request, courselike_string, -1) if course_dir.isdir(): # pylint: disable=no-value-for-parameter shutil.rmtree(course_dir) log.info("Course import %s: Temp data cleared", courselike_key) log.exception("error importing course") return JsonResponse({ 'ErrMsg': str(exception), 'Stage': -1 }, status=400) return JsonResponse({'ImportStatus': 1})
def textbooks_list_handler(request, course_key_string): """ A RESTful handler for textbook collections. GET html: return textbook list page (Backbone application) json: return JSON representation of all textbooks in this course POST json: create a new textbook for this course PUT json: overwrite all textbooks in the course with the given list """ course_key = CourseKey.from_string(course_key_string) course = _get_course_module(course_key, request.user) store = get_modulestore(course.location) 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, }) # from here on down, we know the client has requested JSON if request.method == 'GET': return JsonResponse(course.pdf_textbooks) elif request.method == 'PUT': try: textbooks = validate_textbooks_json(request.body) except TextbookValidationError as err: return JsonResponse({"error": err.message}, status=400) tids = set(t["id"] for t in textbooks if "id" in t) for textbook in textbooks: if not "id" in textbook: tid = assign_textbook_id(textbook, tids) textbook["id"] = tid tids.add(tid) if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs): course.tabs.append(PDFTextbookTabs()) course.pdf_textbooks = textbooks store.update_item(course, request.user.id) return JsonResponse(course.pdf_textbooks) elif request.method == 'POST': # create a new textbook for the course try: textbook = validate_textbook_json(request.body) except TextbookValidationError as err: return JsonResponse({"error": err.message}, status=400) if not textbook.get("id"): tids = set(t["id"] for t in course.pdf_textbooks if "id" in t) textbook["id"] = assign_textbook_id(textbook, tids) existing = course.pdf_textbooks existing.append(textbook) course.pdf_textbooks = existing if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs): 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
def get_url_for_course_key(self, course_id, **kwargs): return reverse_course_url(self.VIEW_NAME, course_id, kwargs)
def create_new_course(request): """ Create a new course. Returns the URL for the course overview page. """ if not auth.has_access(request.user, CourseCreatorRole()): raise PermissionDenied() org = request.json.get('org') number = request.json.get('number') display_name = request.json.get('display_name') run = request.json.get('run') # allow/disable unicode characters in course_id according to settings if not settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID'): if _has_non_ascii_characters(org) or _has_non_ascii_characters( number) or _has_non_ascii_characters(run): return JsonResponse( { 'error': _('Special characters not allowed in organization, course number, and course run.' ) }, status=400) try: course_key = SlashSeparatedCourseKey(org, number, run) # instantiate the CourseDescriptor and then persist it # note: no system to pass if display_name is None: metadata = {} else: metadata = {'display_name': display_name} # Set a unique wiki_slug for newly created courses. To maintain active wiki_slugs for # existing xml courses this cannot be changed in CourseDescriptor. # # TODO get rid of defining wiki slug in this org/course/run specific way and reconcile # w/ xmodule.course_module.CourseDescriptor.__init__ wiki_slug = u"{0}.{1}.{2}".format(course_key.org, course_key.course, course_key.run) definition_data = {'wiki_slug': wiki_slug} # Create the course then fetch it from the modulestore # Check if role permissions group for a course named like this already exists # Important because role groups are case insensitive if CourseRole.course_group_already_exists(course_key): raise InvalidLocationError() fields = {} fields.update(definition_data) fields.update(metadata) # Creating the course raises InvalidLocationError if an existing course with this org/name is found new_course = modulestore('direct').create_course( course_key.org, course_key.offering, fields=fields, ) # can't use auth.add_users here b/c it requires request.user to already have Instructor perms in this course # however, we can assume that b/c this user had authority to create the course, the user can add themselves CourseInstructorRole(new_course.id).add_users(request.user) auth.add_users(request.user, CourseStaffRole(new_course.id), request.user) # seed the forums seed_permissions_roles(new_course.id) # auto-enroll the course creator in the course so that "View Live" will # work. CourseEnrollment.enroll(request.user, new_course.id) _users_assign_default_role(new_course.id) return JsonResponse( {'url': reverse_course_url('course_handler', new_course.id)}) except InvalidLocationError: return JsonResponse({ 'ErrMsg': _('There is already a course defined with the same ' 'organization, course number, and course run. Please ' 'change either organization or course number to be unique.'), 'OrgErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), 'CourseErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), }) except InvalidKeyError as error: return JsonResponse({ "ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format( name=display_name, err=error.message) })
def main_course_page(step): main_page_link = reverse_course_url('course_handler', world.scenario_dict['COURSE'].id) world.visit(main_page_link) assert_in('Course Outline', world.css_text('h1.page-header'))
def certificates_list_handler(request, course_key_string): """ A RESTful handler for Course Certificates GET html: return Certificates list page (Backbone application) POST json: create new Certificate """ course_key = CourseKey.from_string(course_key_string) store = modulestore() with store.bulk_operations(course_key): try: course = _get_course_and_check_access(course_key, request.user) except PermissionDenied: msg = _('PermissionDenied: Failed in authenticating {user}' ).format(user=request.user) return JsonResponse({"error": msg}, status=403) if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): certificate_url = reverse_course_url( 'certificates.certificates_list_handler', course_key) course_outline_url = reverse_course_url('course_handler', course_key) upload_asset_url = reverse_course_url('assets_handler', course_key) activation_handler_url = reverse_course_url( handler_name='certificates.certificate_activation_handler', course_key=course_key) course_modes = [ mode.slug for mode in CourseMode.modes_for_course(course.id) ] certificate_web_view_url = get_lms_link_for_certificate_web_view( user_id=request.user.id, course_key=course_key, mode=course_modes[ 0] # CourseMode.modes_for_course returns default mode 'honor' if doesn't find anyone. ) certificates = None is_active = False if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): certificates = CertificateManager.get_certificates(course) # we are assuming only one certificate in certificates collection. for certificate in certificates: is_active = certificate.get('is_active', False) break return render_to_response( 'certificates.html', { 'context_course': course, 'certificate_url': certificate_url, 'course_outline_url': course_outline_url, 'upload_asset_url': upload_asset_url, 'certificates': json.dumps(certificates), 'course_modes': course_modes, 'certificate_web_view_url': certificate_web_view_url, 'is_active': is_active, 'certificate_activation_handler_url': activation_handler_url }) elif "application/json" in request.META.get('HTTP_ACCEPT'): # Retrieve the list of certificates for the specified course if request.method == 'GET': certificates = CertificateManager.get_certificates(course) return JsonResponse(certificates, encoder=EdxJSONEncoder) elif request.method == 'POST': # Add a new certificate to the specified course try: new_certificate = CertificateManager.deserialize_certificate( course, request.body) except CertificateValidationError as err: return JsonResponse({"error": err.message}, status=400) if course.certificates.get('certificates') is None: course.certificates['certificates'] = [] course.certificates['certificates'].append( new_certificate.certificate_data) response = JsonResponse( CertificateManager.serialize_certificate(new_certificate), status=201) response["Location"] = reverse_course_url( 'certificates.certificates_detail_handler', course.id, kwargs={'certificate_id': new_certificate.id} # pylint: disable=no-member ) store.update_item(course, request.user.id) CertificateManager.track_event( 'created', { 'course_id': unicode(course.id), 'configuration_id': new_certificate.id }) course = _get_course_and_check_access(course_key, request.user) return response else: return HttpResponse(status=406)
def setUp(self): super(AssetsTestCase, self).setUp() self.url = reverse_course_url('assets_handler', self.course.id)
def _import_handler(request, courselike_key, root_name, successful_url, context_name, courselike_module, import_func): """ Parameterized function containing the meat of import_handler. """ 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: # Do everything in a try-except block to make sure everything is properly cleaned up. try: data_root = path(settings.GITHUB_REPO_ROOT) subdir = base64.urlsafe_b64encode(repr(courselike_key)) course_dir = data_root / subdir filename = request.FILES['course-data'].name # Use sessions to keep info about import progress session_status = request.session.setdefault( "import_status", {}) courselike_string = unicode(courselike_key) + filename _save_request_status(request, courselike_string, 0) # If the course has an entrance exam then remove it and its corresponding milestone. # current course state before import. if root_name == COURSE_ROOT: if courselike_module.entrance_exam_enabled: remove_entrance_exam_milestone_reference( request, courselike_key) log.info( "entrance exam milestone content reference for course %s has been removed", courselike_module.id) if not filename.endswith('.tar.gz'): _save_request_status(request, courselike_string, -1) return JsonResponse( { 'ErrMsg': _('We only support uploading a .tar.gz file.'), 'Stage': -1 }, status=415) temp_filepath = course_dir / filename if not course_dir.isdir(): os.mkdir(course_dir) logging.debug('importing course to {0}'.format(temp_filepath)) # Get upload chunks byte ranges try: matches = CONTENT_RE.search( request.META["HTTP_CONTENT_RANGE"]) content_range = matches.groupdict() except KeyError: # Single chunk # no Content-Range header, so make one that will work content_range = {'start': 0, 'stop': 1, 'end': 2} # stream out the uploaded files in chunks to disk if int(content_range['start']) == 0: mode = "wb+" else: mode = "ab+" size = os.path.getsize(temp_filepath) # Check to make sure we haven't missed a chunk # This shouldn't happen, even if different instances are handling # the same session, but it's always better to catch errors earlier. if size < int(content_range['start']): _save_request_status(request, courselike_string, -1) log.warning( "Reported range %s does not match size downloaded so far %s", content_range['start'], size) return JsonResponse( { 'ErrMsg': _('File upload corrupted. Please try again'), 'Stage': -1 }, status=409) # The last request sometimes comes twice. This happens because # nginx sends a 499 error code when the response takes too long. elif size > int(content_range['stop']) and size == int( content_range['end']): return JsonResponse({'ImportStatus': 1}) with open(temp_filepath, mode) as temp_file: for chunk in request.FILES['course-data'].chunks(): temp_file.write(chunk) size = os.path.getsize(temp_filepath) if int(content_range['stop']) != int(content_range['end']) - 1: # More chunks coming return JsonResponse({ "files": [{ "name": filename, "size": size, "deleteUrl": "", "deleteType": "", "url": reverse_course_url('import_handler', courselike_key), "thumbnailUrl": "" }] }) # Send errors to client with stage at which error occurred. except Exception as exception: # pylint: disable=broad-except _save_request_status(request, courselike_string, -1) if course_dir.isdir(): shutil.rmtree(course_dir) log.info("Course import %s: Temp data cleared", courselike_key) log.exception("error importing course") return JsonResponse({ 'ErrMsg': str(exception), 'Stage': -1 }, status=400) # try-finally block for proper clean up after receiving last chunk. try: # This was the last chunk. log.info("Course import %s: Upload complete", courselike_key) _save_request_status(request, courselike_string, 1) tar_file = tarfile.open(temp_filepath) try: safetar_extractall(tar_file, (course_dir + '/').encode('utf-8')) except SuspiciousOperation as exc: _save_request_status(request, courselike_string, -1) return JsonResponse( { 'ErrMsg': 'Unsafe tar file. Aborting import.', 'SuspiciousFileOperationMsg': exc.args[0], 'Stage': -1 }, status=400) finally: tar_file.close() log.info("Course import %s: Uploaded file extracted", courselike_key) _save_request_status(request, courselike_string, 2) # find the 'course.xml' file def get_all_files(directory): """ For each file in the directory, yield a 2-tuple of (file-name, directory-path) """ for dirpath, _dirnames, filenames in os.walk(directory): for filename in filenames: yield (filename, dirpath) def get_dir_for_fname(directory, filename): """ Returns the dirpath for the first file found in the directory with the given name. If there is no file in the directory with the specified name, return None. """ for fname, dirpath in get_all_files(directory): if fname == filename: return dirpath return None dirpath = get_dir_for_fname(course_dir, root_name) if not dirpath: _save_request_status(request, courselike_string, -2) return JsonResponse( { 'ErrMsg': _('Could not find the {0} file in the package.'). format(root_name), 'Stage': -2 }, status=415) dirpath = os.path.relpath(dirpath, data_root) logging.debug('found %s at %s', root_name, dirpath) log.info("Course import %s: Extracted file verified", courselike_key) _save_request_status(request, courselike_string, 3) with dog_stats_api.timer( 'courselike_import.time', tags=[u"courselike:{}".format(courselike_key)]): courselike_items = import_func( modulestore(), request.user.id, settings.GITHUB_REPO_ROOT, [dirpath], load_error_modules=False, static_content_store=contentstore(), target_id=courselike_key) new_location = courselike_items[0].location logging.debug('new course at %s', new_location) log.info("Course import %s: Course import successful", courselike_key) _save_request_status(request, courselike_string, 4) # Send errors to client with stage at which error occurred. except Exception as exception: # pylint: disable=broad-except log.exception("error importing course") return JsonResponse( { 'ErrMsg': str(exception), 'Stage': -session_status[courselike_string] }, status=400) finally: if course_dir.isdir(): shutil.rmtree(course_dir) log.info("Course import %s: Temp data cleared", courselike_key) # set failed stage number with negative sign in case of unsuccessful import if session_status[courselike_string] != 4: _save_request_status( request, courselike_string, -abs(session_status[courselike_string])) # status == 4 represents that course has been imported successfully. if session_status[ courselike_string] == 4 and root_name == COURSE_ROOT: # Reload the course so we have the latest state course = modulestore().get_course(courselike_key) if course.entrance_exam_enabled: entrance_exam_chapter = modulestore().get_items( course.id, qualifiers={'category': 'chapter'}, settings={'is_entrance_exam': True})[0] metadata = { 'entrance_exam_id': unicode(entrance_exam_chapter.location) } CourseMetadata.update_from_dict( metadata, course, request.user) add_entrance_exam_milestone(course.id, entrance_exam_chapter) log.info("Course %s Entrance exam imported", course.id) return JsonResponse({'Status': 'OK'}) 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 container_handler(request, usage_key_string): """ The restful handler for container xblock requests. GET html: returns the HTML page for editing a container json: not currently supported """ if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): try: usage_key = UsageKey.from_string(usage_key_string) except InvalidKeyError: # Raise Http404 on invalid 'usage_key_string' raise Http404 with modulestore().bulk_operations(usage_key.course_key): try: course, xblock, lms_link, preview_lms_link = _get_item_in_course( request, usage_key) except ItemNotFoundError: return HttpResponseBadRequest() component_templates = get_component_templates(course) ancestor_xblocks = [] parent = get_parent_xblock(xblock) action = request.GET.get('action', 'view') 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): unit = parent ancestor_xblocks.append(parent) parent = get_parent_xblock(parent) ancestor_xblocks.reverse() assert unit is not None, "Could not determine unit page" subsection = get_parent_xblock(unit) assert subsection is not None, "Could not determine parent subsection from unit " + six.text_type( unit.location) section = get_parent_xblock(subsection) assert section is not None, "Could not determine ancestor section from unit " + six.text_type( 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. xblock_info = create_xblock_info( xblock, include_ancestor_info=is_unit_page) if is_unit_page: add_container_page_publishing_info(xblock, xblock_info) # need to figure out where this item is in the list of children as the # preview will need this index = 1 for child in subsection.get_children(): if child.location == unit.location: break index += 1 return render_to_response( 'container.html', { 'language_code': request.LANGUAGE_CODE, 'context_course': course, # Needed only for display of menus at top of page. 'action': action, 'xblock': xblock, 'xblock_locator': xblock.location, 'unit': unit, 'is_unit_page': is_unit_page, 'subsection': subsection, 'section': section, 'new_unit_category': 'vertical', 'outline_url': '{url}?format=concise'.format( url=reverse_course_url('course_handler', course.id)), 'ancestor_xblocks': ancestor_xblocks, 'component_templates': component_templates, 'xblock_info': xblock_info, 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, 'templates': CONTAINER_TEMPLATES }) else: return HttpResponseBadRequest("Only supports HTML requests")
def videos_index_html(course, pagination_conf=None): """ Returns an HTML page to display previous video uploads and allow new ones """ is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled( course.id) previous_uploads, pagination_context = _get_index_videos( course, pagination_conf) context = { 'context_course': course, 'image_upload_url': reverse_course_url('video_images_handler', six.text_type(course.id)), 'video_handler_url': reverse_course_url('videos_handler', six.text_type(course.id)), 'encodings_download_url': reverse_course_url('video_encodings_download', six.text_type(course.id)), 'default_video_image_url': _get_default_video_image_url(), 'previous_uploads': previous_uploads, 'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0), 'video_supported_file_formats': list(VIDEO_SUPPORTED_FILE_FORMATS.keys()), 'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB, 'video_image_settings': { 'video_image_upload_enabled': WAFFLE_SWITCHES.is_enabled(VIDEO_IMAGE_UPLOAD_ENABLED), 'max_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES'], 'min_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'], 'max_width': settings.VIDEO_IMAGE_MAX_WIDTH, 'max_height': settings.VIDEO_IMAGE_MAX_HEIGHT, 'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS }, 'is_video_transcript_enabled': is_video_transcript_enabled, 'active_transcript_preferences': None, 'transcript_credentials': None, 'transcript_available_languages': get_all_transcript_languages(), 'video_transcript_settings': { 'transcript_download_handler_url': reverse('transcript_download_handler'), 'transcript_upload_handler_url': reverse('transcript_upload_handler'), 'transcript_delete_handler_url': reverse_course_url('transcript_delete_handler', six.text_type(course.id)), 'trancript_download_file_format': Transcript.SRT }, 'pagination_context': pagination_context } if is_video_transcript_enabled: context['video_transcript_settings'].update({ 'transcript_preferences_handler_url': reverse_course_url('transcript_preferences_handler', six.text_type(course.id)), 'transcript_credentials_handler_url': reverse_course_url('transcript_credentials_handler', six.text_type(course.id)), 'transcription_plans': get_3rd_party_transcription_plans(), }) context['active_transcript_preferences'] = get_transcript_preferences( six.text_type(course.id)) # Cached state for transcript providers' credentials (org-specific) context[ 'transcript_credentials'] = get_transcript_credentials_state_for_org( course.id.org) return render_to_response('videos_index.html', context)
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 certificates_list_handler(request, course_key_string): """ A RESTful handler for Course Certificates GET html: return Certificates list page (Backbone application) POST json: create new Certificate """ course_key = CourseKey.from_string(course_key_string) store = modulestore() with store.bulk_operations(course_key): try: course = _get_course_and_check_access(course_key, request.user) except PermissionDenied: msg = _('PermissionDenied: Failed in authenticating {user}' ).format(user=request.user) return JsonResponse({"error": msg}, status=403) if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): certificate_url = reverse_course_url('certificates_list_handler', course_key) course_outline_url = reverse_course_url('course_handler', course_key) upload_asset_url = reverse_course_url('assets_handler', course_key) activation_handler_url = reverse_course_url( handler_name='certificate_activation_handler', course_key=course_key) course_modes = [ mode.slug for mode in CourseMode.modes_for_course(course_id=course.id, include_expired=True) if mode.slug != 'audit' ] has_certificate_modes = len(course_modes) > 0 if has_certificate_modes: certificate_web_view_url = get_lms_link_for_certificate_web_view( user_id=request.user.id, course_key=course_key, mode=course_modes[ 0] # CourseMode.modes_for_course returns default mode if doesn't find anyone. ) else: certificate_web_view_url = None is_active, certificates = CertificateManager.is_activated(course) return render_to_response( 'certificates.html', { 'context_course': course, 'certificate_url': certificate_url, 'course_outline_url': course_outline_url, 'upload_asset_url': upload_asset_url, 'certificates': certificates, 'has_certificate_modes': has_certificate_modes, 'course_modes': course_modes, 'certificate_web_view_url': certificate_web_view_url, 'is_active': is_active, 'is_global_staff': GlobalStaff().has_user(request.user), 'certificate_activation_handler_url': activation_handler_url }) elif "application/json" in request.META.get('HTTP_ACCEPT'): # Retrieve the list of certificates for the specified course if request.method == 'GET': certificates = CertificateManager.get_certificates(course) return JsonResponse(certificates, encoder=EdxJSONEncoder) elif request.method == 'POST': # Add a new certificate to the specified course try: new_certificate = CertificateManager.deserialize_certificate( course, request.body) except CertificateValidationError as err: return JsonResponse({"error": text_type(err)}, status=400) if course.certificates.get('certificates') is None: course.certificates['certificates'] = [] course.certificates['certificates'].append( new_certificate.certificate_data) response = JsonResponse( CertificateManager.serialize_certificate(new_certificate), status=201) response["Location"] = reverse_course_url( 'certificates_detail_handler', course.id, kwargs={'certificate_id': new_certificate.id}) store.update_item(course, request.user.id) CertificateManager.track_event( 'created', { 'course_id': unicode(course.id), 'configuration_id': new_certificate.id }) course = _get_course_and_check_access(course_key, request.user) return response else: return HttpResponse(status=406)
def course_team_url(self, email=None): return reverse_course_url( 'course_team_handler', self.course.id, kwargs={'email': email} if email else {} )
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) if not has_course_access(request.user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key) # 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')) export_url = reverse_course_url('export_handler', course_key) + '?_accept=application/x-tgz' if 'application/x-tgz' in requested_format: name = course_module.url_name export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz") root_dir = path(mkdtemp()) try: export_to_xml(modulestore('direct'), contentstore(), course_module.id, root_dir, name, modulestore()) logging.debug('tar file being generated at {0}'.format( export_file.name)) with tarfile.open(name=export_file.name, mode='w:gz') as tar_file: tar_file.add(root_dir / name, arcname=name) except SerializationError as exc: log.exception('There was an error exporting course %s', course_module.id) unit = None failed_item = None parent = None try: failed_item = modulestore().get_item(exc.location) parent_locs = modulestore().get_parent_locations( failed_item.location) if len(parent_locs) > 0: parent = modulestore().get_item(parent_locs[0]) if parent.location.category == 'vertical': unit = parent except: # pylint: disable=bare-except # if we have a nested exception, then we'll show the more generic error message pass return render_to_response( 'export.html', { 'context_course': course_module, 'in_err': True, 'raw_err_msg': str(exc), 'failed_module': failed_item, 'unit': unit, 'edit_unit_url': reverse_usage_url("unit_handler", parent.location) if parent else "", 'course_home_url': reverse_course_url("course_handler", course_key), 'export_url': export_url }) except Exception as exc: log.exception('There was an error exporting course %s', course_module.id) return render_to_response( 'export.html', { 'context_course': course_module, 'in_err': True, 'unit': None, 'raw_err_msg': str(exc), 'course_home_url': reverse_course_url("course_handler", course_key), 'export_url': export_url }) finally: shutil.rmtree(root_dir / name) wrapper = FileWrapper(export_file) response = HttpResponse(wrapper, content_type='application/x-tgz') response[ 'Content-Disposition'] = 'attachment; filename=%s' % os.path.basename( export_file.name) response['Content-Length'] = os.path.getsize(export_file.name) return response elif 'text/html' in requested_format: return render_to_response('export.html', { 'context_course': course_module, 'export_url': export_url }) else: # Only HTML or x-tgz request formats are supported (no JSON). return HttpResponse(status=406)
def setUp(self): """ Sets up the test course. """ super(ExportTestCase, self).setUp() self.url = reverse_course_url('export_handler', self.course.id)
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 """ course_key = CourseKey.from_string(course_key_string) if not has_course_access(request.user, course_key): raise PermissionDenied() if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if request.method == 'GET': raise NotImplementedError('coming soon') else: data_root = path(settings.GITHUB_REPO_ROOT) course_subdir = "{0}-{1}-{2}".format(course_key.org, course_key.course, course_key.run) course_dir = data_root / course_subdir filename = request.FILES['course-data'].name if not filename.endswith('.tar.gz'): return JsonResponse( { 'ErrMsg': _('We only support uploading a .tar.gz file.'), 'Stage': 1 }, status=415) temp_filepath = course_dir / filename if not course_dir.isdir(): os.mkdir(course_dir) logging.debug('importing course to {0}'.format(temp_filepath)) # Get upload chunks byte ranges try: matches = CONTENT_RE.search(request.META["HTTP_CONTENT_RANGE"]) content_range = matches.groupdict() except KeyError: # Single chunk # no Content-Range header, so make one that will work content_range = {'start': 0, 'stop': 1, 'end': 2} # stream out the uploaded files in chunks to disk if int(content_range['start']) == 0: mode = "wb+" else: mode = "ab+" size = os.path.getsize(temp_filepath) # Check to make sure we haven't missed a chunk # This shouldn't happen, even if different instances are handling # the same session, but it's always better to catch errors earlier. if size < int(content_range['start']): log.warning( "Reported range %s does not match size downloaded so far %s", content_range['start'], size) return JsonResponse( { 'ErrMsg': _('File upload corrupted. Please try again'), 'Stage': 1 }, status=409) # The last request sometimes comes twice. This happens because # nginx sends a 499 error code when the response takes too long. elif size > int(content_range['stop']) and size == int( content_range['end']): return JsonResponse({'ImportStatus': 1}) with open(temp_filepath, mode) as temp_file: for chunk in request.FILES['course-data'].chunks(): temp_file.write(chunk) size = os.path.getsize(temp_filepath) if int(content_range['stop']) != int(content_range['end']) - 1: # More chunks coming return JsonResponse({ "files": [{ "name": filename, "size": size, "deleteUrl": "", "deleteType": "", "url": reverse_course_url('import_handler', course_key), "thumbnailUrl": "" }] }) else: # This was the last chunk. # Use sessions to keep info about import progress session_status = request.session.setdefault( "import_status", {}) key = unicode(course_key) + filename session_status[key] = 1 request.session.modified = True # Do everything from now on in a try-finally block to make sure # everything is properly cleaned up. try: tar_file = tarfile.open(temp_filepath) try: safetar_extractall(tar_file, (course_dir + '/').encode('utf-8')) except SuspiciousOperation as exc: return JsonResponse( { 'ErrMsg': 'Unsafe tar file. Aborting import.', 'SuspiciousFileOperationMsg': exc.args[0], 'Stage': 1 }, status=400) finally: tar_file.close() session_status[key] = 2 request.session.modified = True # find the 'course.xml' file def get_all_files(directory): """ For each file in the directory, yield a 2-tuple of (file-name, directory-path) """ for dirpath, _dirnames, filenames in os.walk( directory): for filename in filenames: yield (filename, dirpath) def get_dir_for_fname(directory, filename): """ Returns the dirpath for the first file found in the directory with the given name. If there is no file in the directory with the specified name, return None. """ for fname, dirpath in get_all_files(directory): if fname == filename: return dirpath return None fname = "course.xml" dirpath = get_dir_for_fname(course_dir, fname) if not dirpath: return JsonResponse( { 'ErrMsg': _('Could not find the course.xml file in the package.' ), 'Stage': 2 }, status=415) logging.debug('found course.xml at {0}'.format(dirpath)) if dirpath != course_dir: for fname in os.listdir(dirpath): shutil.move(dirpath / fname, course_dir) _module_store, course_items = import_from_xml( modulestore('direct'), settings.GITHUB_REPO_ROOT, [course_subdir], load_error_modules=False, static_content_store=contentstore(), target_course_id=course_key, draft_store=modulestore()) new_location = course_items[0].location logging.debug('new course at {0}'.format(new_location)) session_status[key] = 3 request.session.modified = True # Send errors to client with stage at which error occurred. except Exception as exception: # pylint: disable=W0703 log.exception("error importing course") return JsonResponse( { 'ErrMsg': str(exception), 'Stage': session_status[key] }, status=400) finally: shutil.rmtree(course_dir) return JsonResponse({'Status': 'OK'}) elif request.method == 'GET': # assume html course_module = modulestore().get_course(course_key) return render_to_response( 'import.html', { 'context_course': course_module, 'successful_import_redirect_url': reverse_course_url('course_handler', course_key), 'import_status_url': reverse_course_url("import_status_handler", course_key, kwargs={'filename': "fillerName"}), }) else: return HttpResponseNotFound()
def get_url_for_course_key(self, course_key, kwargs=None): """Return video handler URL for the given course""" return reverse_course_url(self.VIEW_NAME, course_key, kwargs)
def get_url(course_id, handler_name='settings_handler'): return reverse_course_url(handler_name, course_id)