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)
Example #2
0
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)
Example #5
0
    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)
Example #7
0
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)
Example #9
0
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),
        },
    )
Example #10
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)
Example #13
0
    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)
            )
Example #14
0
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)
Example #19
0
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
        }
    )
Example #20
0
 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),
            )
Example #22
0
    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)
Example #24
0
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)
Example #26
0
    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))
Example #28
0
    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)
Example #32
0
    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))
Example #34
0
 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)
Example #37
0
 def setUp(self):
     "Set the URL for tests"
     super(TextbookIndexTestCase, self).setUp()
     self.url = reverse_course_url('textbooks_list_handler', self.course.id)
Example #38
0
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)
Example #39
0
    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)
Example #40
0
    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)
Example #43
0
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})
Example #44
0
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
Example #45
0
 def get_url_for_course_key(self, course_id, **kwargs):
     return reverse_course_url(self.VIEW_NAME, course_id, kwargs)
Example #46
0
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)
        })
Example #47
0
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'))
Example #48
0
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)
Example #49
0
 def setUp(self):
     super(AssetsTestCase, self).setUp()
     self.url = reverse_course_url('assets_handler', self.course.id)
Example #50
0
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()
Example #51
0
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")
Example #52
0
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)
Example #53
0
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)
Example #54
0
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)
Example #55
0
 def course_team_url(self, email=None):
     return reverse_course_url(
         'course_team_handler', self.course.id,
         kwargs={'email': email} if email else {}
     )
Example #56
0
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)
Example #57
0
 def setUp(self):
     """
     Sets up the test course.
     """
     super(ExportTestCase, self).setUp()
     self.url = reverse_course_url('export_handler', self.course.id)
Example #58
0
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()
Example #59
0
 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)
Example #60
0
def get_url(course_id, handler_name='settings_handler'):
    return reverse_course_url(handler_name, course_id)