Пример #1
0
 def get_location_from_path(path):
     """
     Generate an AssetKey for the given path (old c4x/org/course/asset/name syntax)
     """
     try:
         return AssetKey.from_string(path)
     except InvalidKeyError:
         # TODO - re-address this once LMS-11198 is tackled.
         if path.startswith('/'):
             # try stripping off the leading slash and try again
             return AssetKey.from_string(path[1:])
Пример #2
0
 def get_location_from_path(path):
     """
     Generate an AssetKey for the given path (old c4x/org/course/asset/name syntax)
     """
     try:
         return AssetKey.from_string(path)
     except InvalidKeyError:
         # TODO - re-address this once LMS-11198 is tackled.
         if path.startswith('/'):
             # try stripping off the leading slash and try again
             return AssetKey.from_string(path[1:])
Пример #3
0
 def from_xml(self, node):
     """
     Walk the etree XML node and fill in the asset metadata.
     The node should be a top-level "asset" element.
     """
     for child in node:
         qname = etree.QName(child)
         tag = qname.localname
         if tag in self.ALL_ATTRS:
             value = child.text
             if tag == 'asset_id':
                 # Locator.
                 value = AssetKey.from_string(value)
             elif tag == 'locked':
                 # Boolean.
                 value = True if value == "true" else False
             elif tag in ('created_on', 'edited_on'):
                 # ISO datetime.
                 value = dateutil.parser.parse(value)
             elif tag in ('created_by', 'edited_by'):
                 # Integer representing user id.
                 value = int(value)
             elif tag == 'fields':
                 # Dictionary.
                 value = json.loads(value)
             elif value == 'None':
                 # None.
                 value = None
             setattr(self, tag, value)
Пример #4
0
 def from_xml(self, node):
     """
     Walk the etree XML node and fill in the asset metadata.
     The node should be a top-level "asset" element.
     """
     for child in node:
         qname = etree.QName(child)
         tag = qname.localname
         if tag in self.ALL_ATTRS:
             value = child.text
             if tag == 'asset_id':
                 # Locator.
                 value = AssetKey.from_string(value)
             elif tag == 'locked':
                 # Boolean.
                 value = True if value == "true" else False
             elif tag in ('created_on', 'edited_on'):
                 # ISO datetime.
                 value = dateutil.parser.parse(value)
             elif tag in ('created_by', 'edited_by'):
                 # Integer representing user id.
                 value = int(value)
             elif tag == 'fields':
                 # Dictionary.
                 value = json.loads(value)
             elif value == 'None':
                 # None.
                 value = None
             setattr(self, tag, value)
Пример #5
0
    def copy_all_course_assets(self, source_course_key, dest_course_key):
        """
        See :meth:`.ContentStore.copy_all_course_assets`

        This implementation fairly expensively copies all of the data
        """
        source_query = query_for_course(source_course_key)
        # it'd be great to figure out how to do all of this on the db server and not pull the bits over
        for asset in self.fs_files.find(source_query):
            asset_key = self.make_id_son(asset)
            # don't convert from string until fs access
            source_content = self.fs.get(asset_key)
            if isinstance(asset_key, str):
                asset_key = AssetKey.from_string(asset_key)
                __, asset_key = self.asset_db_key(asset_key)
            # Need to replace dict IDs with SON for chunk lookup to work under Python 3
            # because field order can be different and mongo cares about the order
            if isinstance(source_content._id, dict):  # lint-amnesty, pylint: disable=protected-access
                source_content._file['_id'] = asset_key.copy()  # lint-amnesty, pylint: disable=protected-access
            asset_key['org'] = dest_course_key.org
            asset_key['course'] = dest_course_key.course
            if getattr(dest_course_key, 'deprecated', False):  # remove the run if exists
                if 'run' in asset_key:
                    del asset_key['run']
                asset_id = asset_key
            else:  # add the run, since it's the last field, we're golden
                asset_key['run'] = dest_course_key.run
                asset_id = str(
                    dest_course_key.make_asset_key(asset_key['category'], asset_key['name']).for_branch(None)
                )
            try:
                self.create_asset(source_content, asset_id, asset, asset_key)
            except FileExists:
                self.fs.delete(file_id=asset_id)
                self.create_asset(source_content, asset_id, asset, asset_key)
Пример #6
0
    def get_asset_key_from_path(course_key, path):
        """
        Parses a path, extracting an asset key or creating one.

        Args:
            course_key: key to the course which owns this asset
            path: the path to said content

        Returns:
            AssetKey: the asset key that represents the path
        """

        # Clean up the path, removing any static prefix and any leading slash.
        if path.startswith('/static/'):
            path = path[len('/static/'):]

        # Old-style asset keys start with `/`, so don't try and strip it
        # in that case.
        if not path.startswith('/c4x'):
            path = path.lstrip('/')

        try:
            return AssetKey.from_string(path)
        except InvalidKeyError:
            # If we couldn't parse the path, just let compute_location figure it out.
            # It's most likely a path like /image.png or something.
            return StaticContent.compute_location(course_key, path)
    def test_can_delete_signatory(self, signatory_path):
        """
        Delete an existing certificate signatory
        """
        self._add_course_certificates(count=2, signatory_count=3, asset_path_format=signatory_path)
        certificates = self.course.certificates['certificates']
        signatory = certificates[1].get("signatories")[1]
        image_asset_location = AssetKey.from_string(signatory['signature_image_path'])
        content = contentstore().find(image_asset_location)
        self.assertIsNotNone(content)
        test_url = '{}/signatories/1'.format(self._url(cid=1))
        response = self.client.delete(
            test_url,
            content_type="application/json",
            HTTP_ACCEPT="application/json",
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
        )
        self.assertEqual(response.status_code, 204)
        self.reload_course()

        # Verify that certificates are properly updated in the course.
        certificates = self.course.certificates['certificates']
        self.assertEqual(len(certificates[1].get("signatories")), 2)
        # make sure signatory signature image is deleted too
        self.assertRaises(NotFoundError, contentstore().find, image_asset_location)
Пример #8
0
    def test_can_delete_signatory(self, signatory_path):
        """
        Delete an existing certificate signatory
        """
        self._add_course_certificates(count=2,
                                      signatory_count=3,
                                      asset_path_format=signatory_path)
        certificates = self.course.certificates['certificates']
        signatory = certificates[1].get("signatories")[1]
        image_asset_location = AssetKey.from_string(
            signatory['signature_image_path'])
        content = contentstore().find(image_asset_location)
        self.assertIsNotNone(content)
        test_url = '{}/signatories/1'.format(self._url(cid=1))
        response = self.client.delete(
            test_url,
            content_type="application/json",
            HTTP_ACCEPT="application/json",
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
        )
        self.assertEqual(response.status_code, 204)
        self.reload_course()

        # Verify that certificates are properly updated in the course.
        certificates = self.course.certificates['certificates']
        self.assertEqual(len(certificates[1].get("signatories")), 2)
        # make sure signatory signature image is deleted too
        self.assertRaises(NotFoundError,
                          contentstore().find, image_asset_location)
Пример #9
0
    def get_asset_key_from_path(course_key, path):
        """
        Parses a path, extracting an asset key or creating one.

        Args:
            course_key: key to the course which owns this asset
            path: the path to said content

        Returns:
            AssetKey: the asset key that represents the path
        """

        # Clean up the path, removing any static prefix and any leading slash.
        if path.startswith('/static/'):
            path = path[len('/static/'):]

        # Old-style asset keys start with `/`, so don't try and strip it
        # in that case.
        if not path.startswith('/c4x'):
            path = path.lstrip('/')

        try:
            return AssetKey.from_string(path)
        except InvalidKeyError:
            # If we couldn't parse the path, just let compute_location figure it out.
            # It's most likely a path like /image.png or something.
            return StaticContent.compute_location(course_key, path)
Пример #10
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 = AssetKey.from_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': six.text_type(uploaded_image_url)})
            resp = self.client.delete(test_url, HTTP_ACCEPT="application/json")
            self.assertEquals(resp.status_code, 204)
Пример #11
0
 def test_asset_with_special_character(self, paths):
     for path in paths:
         asset_locator = AssetKey.from_string(path)
         self.assertEquals(
             path,
             unicode(asset_locator),
         )
Пример #12
0
 def _create_fake_images(self, asset_keys):
     """
     Creates fake image files for a list of asset_keys.
     """
     for asset_key_string in asset_keys:
         asset_key = AssetKey.from_string(asset_key_string)
         content = StaticContent(asset_key, "Fake asset", "image/png", "data")
         contentstore().save(content)
Пример #13
0
def editorjs_handler(request, course_key_string=None, asset_key_string=None):
    '''
    The restful handler for assets.
    It allows retrieval of all the assets (as an HTML page), as well as uploading new assets,
    deleting assets, and changing the 'locked' state of an asset.

    GET
        html: return an html page which will show all course assets. Note that only the asset container
            is returned and that the actual assets are filled in with a client-side request.
        json: returns a page of assets. The following parameters are supported:
            page: the desired page of results (defaults to 0)
            page_size: the number of items per page (defaults to 50)
            sort: the asset field to sort by (defaults to 'date_added')
            direction: the sort direction (defaults to 'descending')
            asset_type: the file type to filter items to (defaults to All)
            text_search: string to filter results by file name (defaults to '')
    POST
        json: create (or update?) an asset. The only updating that can be done is changing the lock state.
    PUT
        json: update the locked state of an asset
    DELETE
        json: delete an asset
    '''
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    response_format = _get_response_format(request)
    if _request_response_format_is_json(request, response_format):
        if request.method == 'GET':
            if request.method == 'GET' and request.GET.get('url'):
                url = request.GET['url']
                return JsonResponse({
                    "success": 1,
                    "meta": {
                        'url': url,
                        'display_name': url,
                        'content_type': url,
                        'date_added': url,
                        'asset_url': url,
                        'external_url': url,
                        'portable_url': url,
                        'thumbnail': url,
                        'locked': url,
                        'id': url
                    }
                })
            else:
                return _assets_json(request, course_key)

        asset_key = AssetKey.from_string(
            asset_key_string) if asset_key_string else None
        return _update_asset(request, course_key, asset_key)

    elif request.method == 'GET':  # assume html
        return _asset_index(request, course_key)

    return HttpResponseNotFound()
Пример #14
0
 def test_replace(self):
     asset_key = AssetKey.from_string('/c4x/o/c/asset/path')
     self.assertEquals(
         'foo',
         asset_key.replace(path='foo').path
     )
     self.assertEquals(
         'bar',
         asset_key.replace(asset_type='bar').asset_type
     )
Пример #15
0
 def _create_fake_images(self, asset_keys):
     """
     Creates fake image files for a list of asset_keys.
     """
     for asset_key_string in asset_keys:
         asset_key = AssetKey.from_string(asset_key_string)
         content = StaticContent(
             asset_key, "Fake asset", "image/png", "data",
         )
         contentstore().save(content)
Пример #16
0
def get_course_ids_from_link(link):
    """
    Get course Ids from link.

    Arguments:
        link(str): The link to extract course id from.
    Returns:
        Course Ids
    """
    asset_str = link.partition('asset-v1')[1] + link.partition('asset-v1')[2]
    return AssetKey.from_string(asset_str).course_key
Пример #17
0
def get_course_ids_from_link(link):
    """
    Get course Ids from link.

    Arguments:
        link(str): The link to extract course id from.
    Returns:
        Course Ids
    """
    asset_str = link.partition('asset-v1')[1] + link.partition('asset-v1')[2]
    return AssetKey.from_string(asset_str).course_key
Пример #18
0
 def check_asset_key(match_obj):
     """
     If this URL's path part is an AssetKey from the same course, rewrite it.
     """
     try:
         asset_key = AssetKey.from_string(
             match_obj.group('maybe_asset_key'))
     except InvalidKeyError:
         return match_obj.group(0)  # Not an asset key; do not rewrite
     if asset_key.course_key == course_id:
         return '/static/' + asset_key.path  # Rewrite this to portable form
     else:
         return match_obj.group(
             0)  # From a different course; do not rewrite
Пример #19
0
    def setUp(self):
        """ Scaffolding """
        super(DeleteAssetTestCase, self).setUp()
        self.url = reverse_course_url('assets_handler', self.course.id)
        # First, upload something.
        self.asset_name = 'delete_test'
        self.asset = self.get_sample_asset(self.asset_name)

        response = self.client.post(self.url, {"name": self.asset_name, "file": self.asset})
        self.assertEquals(response.status_code, 200)
        self.uploaded_url = json.loads(response.content)['asset']['url']

        self.asset_location = AssetKey.from_string(self.uploaded_url)
        self.content = contentstore().find(self.asset_location)
Пример #20
0
def _delete_asset(course_key, asset_key_string):
    """
    Internal method used to create asset key from string and
    remove asset by calling delete_asset method of assets module.
    """
    if asset_key_string:
        try:
            asset_key = AssetKey.from_string(asset_key_string)
        except InvalidKeyError:
            # remove first slash in asset path
            # otherwise it generates InvalidKeyError in case of split modulestore
            if '/' == asset_key_string[0]:
                asset_key_string = asset_key_string[1:]
                try:
                    asset_key = AssetKey.from_string(asset_key_string)
                except InvalidKeyError:
                    # Unable to parse the asset key, log and return
                    LOGGER.info(
                        "In course %r, unable to parse asset key %r, not attempting to delete signatory.",
                        course_key,
                        asset_key_string,
                    )
                    return
            else:
                # Unable to parse the asset key, log and return
                LOGGER.info(
                    "In course %r, unable to parse asset key %r, not attempting to delete signatory.",
                    course_key,
                    asset_key_string,
                )
                return

        try:
            delete_asset(course_key, asset_key)
        # If the asset was not found, it doesn't have to be deleted...
        except AssetNotFoundException:
            pass
Пример #21
0
def _delete_asset(course_key, asset_key_string):
    """
    Internal method used to create asset key from string and
    remove asset by calling delete_asset method of assets module.
    """
    if asset_key_string:
        try:
            asset_key = AssetKey.from_string(asset_key_string)
        except InvalidKeyError:
            # remove first slash in asset path
            # otherwise it generates InvalidKeyError in case of split modulestore
            if '/' == asset_key_string[0]:
                asset_key_string = asset_key_string[1:]
                try:
                    asset_key = AssetKey.from_string(asset_key_string)
                except InvalidKeyError:
                    # Unable to parse the asset key, log and return
                    LOGGER.info(
                        "In course %r, unable to parse asset key %r, not attempting to delete signatory.",
                        course_key,
                        asset_key_string,
                    )
                    return
            else:
                # Unable to parse the asset key, log and return
                LOGGER.info(
                    "In course %r, unable to parse asset key %r, not attempting to delete signatory.",
                    course_key,
                    asset_key_string,
                )
                return

        try:
            delete_asset(course_key, asset_key)
        # If the asset was not found, it doesn't have to be deleted...
        except AssetNotFoundException:
            pass
Пример #22
0
def _delete_asset(course_key, asset_key_string):
    """
    Internal method used to create asset key from string and
    remove asset by calling delete_asset method of assets module.
    """
    if asset_key_string:
        # remove first slash in asset path
        # otherwise it generates InvalidKeyError in case of split modulestore
        if '/' == asset_key_string[0]:
            asset_key_string = asset_key_string[1:]
        asset_key = AssetKey.from_string(asset_key_string)
        try:
            delete_asset(course_key, asset_key)
        # If the asset was not found, it doesn't have to be deleted...
        except AssetNotFoundException:
            pass
Пример #23
0
def _delete_asset(course_key, asset_key_string):
    """
    Internal method used to create asset key from string and
    remove asset by calling delete_asset method of assets module.
    """
    if asset_key_string:
        # remove first slash in asset path
        # otherwise it generates InvalidKeyError in case of split modulestore
        if '/' == asset_key_string[0]:
            asset_key_string = asset_key_string[1:]
        asset_key = AssetKey.from_string(asset_key_string)
        try:
            delete_asset(course_key, asset_key)
        # If the asset was not found, it doesn't have to be deleted...
        except AssetNotFoundException:
            pass
Пример #24
0
    def copy_all_course_assets(self, source_course_key, dest_course_key):
        """
        See :meth:`.ContentStore.copy_all_course_assets`

        This implementation fairly expensively copies all of the data
        """
        source_query = query_for_course(source_course_key)
        # it'd be great to figure out how to do all of this on the db server and not pull the bits over
        for asset in self.fs_files.find(source_query):
            asset_key = self.make_id_son(asset)
            # don't convert from string until fs access
            source_content = self.fs.get(asset_key)
            if isinstance(asset_key, six.string_types):
                asset_key = AssetKey.from_string(asset_key)
                __, asset_key = self.asset_db_key(asset_key)
            # Need to replace dict IDs with SON for chunk lookup to work under Python 3
            # because field order can be different and mongo cares about the order
            if isinstance(source_content._id, dict):
                source_content._file['_id'] = asset_key.copy()
            asset_key['org'] = dest_course_key.org
            asset_key['course'] = dest_course_key.course
            if getattr(dest_course_key, 'deprecated',
                       False):  # remove the run if exists
                if 'run' in asset_key:
                    del asset_key['run']
                asset_id = asset_key
            else:  # add the run, since it's the last field, we're golden
                asset_key['run'] = dest_course_key.run
                asset_id = six.text_type(
                    dest_course_key.make_asset_key(
                        asset_key['category'],
                        asset_key['name']).for_branch(None))

            self.fs.put(
                source_content.read(),
                _id=asset_id,
                filename=asset['filename'],
                content_type=asset['contentType'],
                displayname=asset['displayname'],
                content_son=asset_key,
                # thumbnail is not technically correct but will be functionally correct as the code
                # only looks at the name which is not course relative.
                thumbnail_location=asset['thumbnail_location'],
                import_path=asset['import_path'],
                # getattr b/c caching may mean some pickled instances don't have attr
                locked=asset.get('locked', False))
    def test_get_all_content(self, deprecated):
        """
        Test get_all_content_for_course
        """
        self.set_up_assets(deprecated)
        course1_assets, count = self.contentstore.get_all_content_for_course(self.course1_key)
        self.assertEqual(count, len(self.course1_files), course1_assets)
        for asset in course1_assets:
            parsed = AssetKey.from_string(asset['filename'])
            self.assertIn(parsed.block_id, self.course1_files)

        course1_assets, __ = self.contentstore.get_all_content_for_course(self.course1_key, 1, 1)
        self.assertEqual(len(course1_assets), 1, course1_assets)

        fake_course = CourseLocator('test', 'fake', 'non')
        course_assets, count = self.contentstore.get_all_content_for_course(fake_course)
        self.assertEqual(count, 0)
        self.assertEqual(course_assets, [])
Пример #26
0
    def test_get_all_content(self, deprecated):
        """
        Test get_all_content_for_course
        """
        self.set_up_assets(deprecated)
        course1_assets, count = self.contentstore.get_all_content_for_course(self.course1_key)
        self.assertEqual(count, len(self.course1_files), course1_assets)
        for asset in course1_assets:
            parsed = AssetKey.from_string(asset['filename'])
            self.assertIn(parsed.name, self.course1_files)

        course1_assets, __ = self.contentstore.get_all_content_for_course(self.course1_key, 1, 1)
        self.assertEqual(len(course1_assets), 1, course1_assets)

        fake_course = CourseLocator('test', 'fake', 'non')
        course_assets, count = self.contentstore.get_all_content_for_course(fake_course)
        self.assertEqual(count, 0)
        self.assertEqual(course_assets, [])
Пример #27
0
    def copy_all_course_assets(self, source_course_key, dest_course_key):
        """
        See :meth:`.ContentStore.copy_all_course_assets`

        This implementation fairly expensively copies all of the data
        """
        source_query = query_for_course(source_course_key)
        # it'd be great to figure out how to do all of this on the db server and not pull the bits over
        for asset in self.fs_files.find(source_query):
            asset_key = self.make_id_son(asset)
            # don't convert from string until fs access
            source_content = self.fs.get(asset_key)
            if isinstance(asset_key, basestring):
                asset_key = AssetKey.from_string(asset_key)
                __, asset_key = self.asset_db_key(asset_key)
            asset_key["org"] = dest_course_key.org
            asset_key["course"] = dest_course_key.course
            if getattr(dest_course_key, "deprecated", False):  # remove the run if exists
                if "run" in asset_key:
                    del asset_key["run"]
                asset_id = asset_key
            else:  # add the run, since it's the last field, we're golden
                asset_key["run"] = dest_course_key.run
                asset_id = unicode(
                    dest_course_key.make_asset_key(asset_key["category"], asset_key["name"]).for_branch(None)
                )

            self.fs.put(
                source_content.read(),
                _id=asset_id,
                filename=asset["filename"],
                content_type=asset["contentType"],
                displayname=asset["displayname"],
                content_son=asset_key,
                # thumbnail is not technically correct but will be functionally correct as the code
                # only looks at the name which is not course relative.
                thumbnail_location=asset["thumbnail_location"],
                import_path=asset["import_path"],
                # getattr b/c caching may mean some pickled instances don't have attr
                locked=asset.get("locked", False),
            )
Пример #28
0
    def test_pdf_asset(self):
        module_store = modulestore()
        course_items = import_course_from_xml(
            module_store,
            self.user.id,
            TEST_DATA_DIR, ['toy'],
            static_content_store=contentstore(),
            verbose=True)
        course = course_items[0]
        url = reverse_course_url('assets_handler', course.id)

        # Test valid contentType for pdf asset (textbook.pdf)
        resp = self.client.get(url, HTTP_ACCEPT='application/json')
        self.assertContains(resp, "/c4x/edX/toy/asset/textbook.pdf")
        asset_location = AssetKey.from_string(
            '/c4x/edX/toy/asset/textbook.pdf')
        content = contentstore().find(asset_location)
        # Check after import textbook.pdf has valid contentType ('application/pdf')

        # Note: Actual contentType for textbook.pdf in asset.json is 'text/pdf'
        self.assertEqual(content.content_type, 'application/pdf')
Пример #29
0
    def test_get_all_content(self, deprecated):
        """
        Test get_all_content_for_course
        """
        self.set_up_assets(deprecated)
        course1_assets, count = self.contentstore.get_all_content_for_course(
            self.course1_key)
        assert count == len(self.course1_files), course1_assets
        for asset in course1_assets:
            parsed = AssetKey.from_string(asset['filename'])
            assert parsed.block_id in self.course1_files

        course1_assets, __ = self.contentstore.get_all_content_for_course(
            self.course1_key, 1, 1)
        assert len(course1_assets) == 1, course1_assets

        fake_course = CourseLocator('test', 'fake', 'non')
        course_assets, count = self.contentstore.get_all_content_for_course(
            fake_course)
        assert count == 0
        assert course_assets == []
Пример #30
0
def assets_handler(request, course_key_string=None, asset_key_string=None):
    '''
    The restful handler for assets.
    It allows retrieval of all the assets (as an HTML page), as well as uploading new assets,
    deleting assets, and changing the 'locked' state of an asset.

    GET
        html: return an html page which will show all course assets. Note that only the asset container
            is returned and that the actual assets are filled in with a client-side request.
        json: returns a page of assets. The following parameters are supported:
            page: the desired page of results (defaults to 0)
            page_size: the number of items per page (defaults to 50)
            sort: the asset field to sort by (defaults to 'date_added')
            direction: the sort direction (defaults to 'descending')
            asset_type: the file type to filter items to (defaults to All)
            text_search: string to filter results by file name (defaults to '')
    POST
        json: create (or update?) an asset. The only updating that can be done is changing the lock state.
    PUT
        json: update the locked state of an asset
    DELETE
        json: delete an asset
    '''
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    response_format = _get_response_format(request)
    if _request_response_format_is_json(request, response_format):
        if request.method == 'GET':
            return _assets_json(request, course_key)

        asset_key = AssetKey.from_string(asset_key_string) if asset_key_string else None
        return _update_asset(request, course_key, asset_key)

    elif request.method == 'GET':  # assume html
        return _asset_index(request, course_key)

    return HttpResponseNotFound()
Пример #31
0
def assets_handler(request, course_key_string=None, asset_key_string=None):
    """
    The restful handler for assets.
    It allows retrieval of all the assets (as an HTML page), as well as uploading new assets,
    deleting assets, and changing the "locked" state of an asset.

    GET
        html: return an html page which will show all course assets. Note that only the asset container
            is returned and that the actual assets are filled in with a client-side request.
        json: returns a page of assets. The following parameters are supported:
            page: the desired page of results (defaults to 0)
            page_size: the number of items per page (defaults to 50)
            sort: the asset field to sort by (defaults to "date_added")
            direction: the sort direction (defaults to "descending")
    POST
        json: create (or update?) an asset. The only updating that can be done is changing the lock state.
    PUT
        json: update the locked state of an asset
    DELETE
        json: delete an asset
    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_author_access(request.user, course_key):
        raise PermissionDenied()

    response_format = request.GET.get('format') or request.POST.get(
        'format') or 'html'
    if response_format == 'json' or 'application/json' in request.META.get(
            'HTTP_ACCEPT', 'application/json'):
        if request.method == 'GET':
            return _assets_json(request, course_key)
        else:
            asset_key = AssetKey.from_string(
                asset_key_string) if asset_key_string else None
            return _update_asset(request, course_key, asset_key)
    elif request.method == 'GET':  # assume html
        return _asset_index(request, course_key)
    else:
        return HttpResponseNotFound()
Пример #32
0
def assets_handler(request, course_key_string=None, asset_key_string=None):
    """
    The restful handler for assets.
    It allows retrieval of all the assets (as an HTML page), as well as uploading new assets,
    deleting assets, and changing the "locked" state of an asset.

    GET
        html: return an html page which will show all course assets. Note that only the asset container
            is returned and that the actual assets are filled in with a client-side request.
        json: returns a page of assets. The following parameters are supported:
            page: the desired page of results (defaults to 0)
            page_size: the number of items per page (defaults to 50)
            sort: the asset field to sort by (defaults to "date_added")
            direction: the sort direction (defaults to "descending")
    POST
        json: create (or update?) an asset. The only updating that can be done is changing the lock state.
    PUT
        json: update the locked state of an asset
    DELETE
        json: delete an asset
    """
    course_key = CourseKey.from_string(course_key_string)
    if not has_course_access(request.user, course_key):
        raise PermissionDenied()

    response_format = request.REQUEST.get('format', 'html')
    if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
        if request.method == 'GET':
            return _assets_json(request, course_key)
        else:
            asset_key = AssetKey.from_string(asset_key_string) if asset_key_string else None
            return _update_asset(request, course_key, asset_key)
    elif request.method == 'GET':  # assume html
        return _asset_index(request, course_key)
    else:
        return HttpResponseNotFound()
Пример #33
0
        """Returns the deprecated tag for this Location."""
        return self.DEPRECATED_TAG

    @classmethod
    def _from_deprecated_string(cls, serialized):
        match = cls.ASSET_URL_RE.match(serialized)
        if match is None:
            raise InvalidKeyError(cls, serialized)
        groups = match.groupdict()
        course_key = CourseLocator(groups['org'],
                                   groups['course'],
                                   None,
                                   groups.get('revision', None),
                                   deprecated=True)
        return cls(course_key,
                   groups['category'],
                   groups['name'],
                   deprecated=True)

    def to_deprecated_list_repr(self):
        """
        Thumbnail locations are stored as lists [c4x, org, course, thumbnail, path, None] in contentstore.mongo
        That should be the only use of this method, but the method is general enough to provide the pre-opaque
        Location fields as an array in the old order with the tag.
        """
        return ['c4x', self.org, self.course, self.block_type, self.name, None]


# Register AssetLocator as the deprecated fallback for AssetKey
AssetKey.set_deprecated_fallback(AssetLocator)
Пример #34
0
 def test_replace(self):
     asset_key = AssetKey.from_string('/c4x/o/c/asset/path')
     self.assertEqual('foo', asset_key.replace(path='foo').path)
     self.assertEqual('bar', asset_key.replace(asset_type='bar').asset_type)
Пример #35
0
 def test_deprecated_round_trip_asset_location(self, path):
     self.assertEqual(
         path,
         text_type(AssetKey.from_string(path)),
     )
Пример #36
0
 def test_asset_with_special_character(self, path):
     asset_locator = AssetKey.from_string(path)
     self.assertEqual(
         path,
         str(asset_locator),
     )
Пример #37
0
 def test_asset_with_trailing_whitespace(self, path_fmt, whitespace):
     with self.assertRaises(InvalidKeyError):
         AssetKey.from_string(path_fmt.format(whitespace))
Пример #38
0
    @property
    def tag(self):
        """Returns the deprecated tag for this Location."""
        return self.DEPRECATED_TAG

    @classmethod
    def _from_deprecated_string(cls, serialized):
        match = cls.ASSET_URL_RE.match(serialized)
        if match is None:
            raise InvalidKeyError(cls, serialized)
        groups = match.groupdict()
        course_key = CourseLocator(
            groups['org'],
            groups['course'],
            None,
            groups.get('revision', None),
            deprecated=True
        )
        return cls(course_key, groups['category'], groups['name'], deprecated=True)

    def to_deprecated_list_repr(self):
        """
        Thumbnail locations are stored as lists [c4x, org, course, thumbnail, path, None] in contentstore.mongo
        That should be the only use of this method, but the method is general enough to provide the pre-opaque
        Location fields as an array in the old order with the tag.
        """
        return ['c4x', self.org, self.course, self.block_type, self.name, None]

# Register AssetLocator as the deprecated fallback for AssetKey
AssetKey.set_deprecated_fallback(AssetLocator)
Пример #39
0
class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase):
    """
    Base class for Studio tests that require a logged in user and a course.
    Also provides helper methods for manipulating and verifying the course.
    """
    def setUp(self):
        """
        These tests need a user in the DB so that the django Test Client can log them in.
        The test user is created in the ModuleStoreTestCase setUp method.
        They inherit from the ModuleStoreTestCase class so that the mongodb collection
        will be cleared out before each test case execution and deleted
        afterwards.
        """

        super(CourseTestCase, self).setUp()

        self.client = AjaxEnabledTestClient()
        self.client.login(username=self.user.username,
                          password=self.user_password)

        self.course = CourseFactory.create()

        # initialize the Notification subsystem
        apps.startup_notification_subsystem()

    def create_non_staff_authed_user_client(self, authenticate=True):
        """
        Create a non-staff user, log them in (if authenticate=True), and return the client, user to use for testing.
        """
        nonstaff, password = self.create_non_staff_user()

        client = AjaxEnabledTestClient()
        if authenticate:
            client.login(username=nonstaff.username, password=password)
        return client, nonstaff

    def reload_course(self):
        """
        Reloads the course object from the database
        """
        self.course = self.store.get_course(self.course.id)

    def save_course(self):
        """
        Updates the course object in the database
        """
        self.course.save()
        self.store.update_item(self.course, self.user.id)

    TEST_VERTICAL = 'vertical_test'
    ORPHAN_DRAFT_VERTICAL = 'orphan_draft_vertical'
    ORPHAN_DRAFT_HTML = 'orphan_draft_html'
    PRIVATE_VERTICAL = 'a_private_vertical'
    PUBLISHED_VERTICAL = 'a_published_vertical'
    SEQUENTIAL = 'vertical_sequential'
    DRAFT_HTML = 'draft_html'
    DRAFT_VIDEO = 'draft_video'
    LOCKED_ASSET_KEY = AssetKey.from_string(
        '/c4x/edX/toy/asset/sample_static.html')

    def import_and_populate_course(self):
        """
        Imports the test toy course and populates it with additional test data
        """
        content_store = contentstore()
        import_course_from_xml(self.store,
                               self.user.id,
                               TEST_DATA_DIR, ['toy'],
                               static_content_store=content_store)
        course_id = CourseKey.from_string('/'.join(['edX', 'toy',
                                                    '2012_Fall']))

        # create an Orphan
        # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
        vertical = self.store.get_item(course_id.make_usage_key(
            'vertical', self.TEST_VERTICAL),
                                       depth=1)
        vertical.location = vertical.location.replace(name='no_references')
        self.store.update_item(vertical, self.user.id, allow_not_found=True)
        orphan_vertical = self.store.get_item(vertical.location)
        self.assertEqual(orphan_vertical.location.block_id, 'no_references')
        self.assertEqual(len(orphan_vertical.children), len(vertical.children))

        # create an orphan vertical and html; we already don't try to import
        # the orphaned vertical, but we should make sure we don't import
        # the orphaned vertical's child html, too
        orphan_draft_vertical = self.store.create_item(
            self.user.id, course_id, 'vertical', self.ORPHAN_DRAFT_VERTICAL)
        orphan_draft_html = self.store.create_item(self.user.id, course_id,
                                                   'html',
                                                   self.ORPHAN_DRAFT_HTML)
        orphan_draft_vertical.children.append(orphan_draft_html.location)
        self.store.update_item(orphan_draft_vertical, self.user.id)

        # create a Draft vertical
        vertical = self.store.get_item(course_id.make_usage_key(
            'vertical', self.TEST_VERTICAL),
                                       depth=1)
        draft_vertical = self.store.convert_to_draft(vertical.location,
                                                     self.user.id)
        self.assertTrue(self.store.has_published_version(draft_vertical))

        # create a Private (draft only) vertical
        private_vertical = self.store.create_item(self.user.id, course_id,
                                                  'vertical',
                                                  self.PRIVATE_VERTICAL)
        self.assertFalse(self.store.has_published_version(private_vertical))

        # create a Published (no draft) vertical
        public_vertical = self.store.create_item(self.user.id, course_id,
                                                 'vertical',
                                                 self.PUBLISHED_VERTICAL)
        public_vertical = self.store.publish(public_vertical.location,
                                             self.user.id)
        self.assertTrue(self.store.has_published_version(public_vertical))

        # add the new private and new public as children of the sequential
        sequential = self.store.get_item(
            course_id.make_usage_key('sequential', self.SEQUENTIAL))
        sequential.children.append(private_vertical.location)
        sequential.children.append(public_vertical.location)
        self.store.update_item(sequential, self.user.id)

        # create an html and video component to make drafts:
        draft_html = self.store.create_item(self.user.id, course_id, 'html',
                                            self.DRAFT_HTML)
        draft_video = self.store.create_item(self.user.id, course_id, 'video',
                                             self.DRAFT_VIDEO)

        # add them as children to the public_vertical
        public_vertical.children.append(draft_html.location)
        public_vertical.children.append(draft_video.location)
        self.store.update_item(public_vertical, self.user.id)
        # publish changes to vertical
        self.store.publish(public_vertical.location, self.user.id)
        # convert html/video to draft
        self.store.convert_to_draft(draft_html.location, self.user.id)
        self.store.convert_to_draft(draft_video.location, self.user.id)

        # lock an asset
        content_store.set_attr(self.LOCKED_ASSET_KEY, 'locked', True)

        # create a non-portable link - should be rewritten in new courses
        html_module = self.store.get_item(
            course_id.make_usage_key('html', 'nonportable'))
        new_data = html_module.data = html_module.data.replace(
            '/static/', '/c4x/{0}/{1}/asset/'.format(course_id.org,
                                                     course_id.course))
        self.store.update_item(html_module, self.user.id)

        html_module = self.store.get_item(html_module.location)
        self.assertEqual(new_data, html_module.data)

        return course_id

    def check_populated_course(self, course_id):
        """
        Verifies the content of the given course, per data that was populated in import_and_populate_course
        """
        items = self.store.get_items(
            course_id,
            qualifiers={'category': 'vertical'},
            revision=ModuleStoreEnum.RevisionOption.published_only)
        self.check_verticals(items)

        def verify_item_publish_state(item, publish_state):
            """Verifies the publish state of the item is as expected."""
            self.assertEqual(self.store.has_published_version(item),
                             publish_state)

        def get_and_verify_publish_state(item_type, item_name, publish_state):
            """
            Gets the given item from the store and verifies the publish state
            of the item is as expected.
            """
            item = self.store.get_item(
                course_id.make_usage_key(item_type, item_name))
            verify_item_publish_state(item, publish_state)
            return item

        # verify draft vertical has a published version with published children
        vertical = get_and_verify_publish_state('vertical', self.TEST_VERTICAL,
                                                True)
        for child in vertical.get_children():
            verify_item_publish_state(child, True)

        # verify that it has a draft too
        self.assertTrue(getattr(vertical, "is_draft", False))

        # make sure that we don't have a sequential that is in draft mode
        sequential = get_and_verify_publish_state('sequential',
                                                  self.SEQUENTIAL, True)
        self.assertFalse(getattr(sequential, "is_draft", False))

        # verify that we have the private vertical
        private_vertical = get_and_verify_publish_state(
            'vertical', self.PRIVATE_VERTICAL, False)

        # verify that we have the public vertical
        public_vertical = get_and_verify_publish_state('vertical',
                                                       self.PUBLISHED_VERTICAL,
                                                       True)

        # verify that we have the draft html
        draft_html = self.store.get_item(
            course_id.make_usage_key('html', self.DRAFT_HTML))
        self.assertTrue(getattr(draft_html, 'is_draft', False))

        # verify that we have the draft video
        draft_video = self.store.get_item(
            course_id.make_usage_key('video', self.DRAFT_VIDEO))
        self.assertTrue(getattr(draft_video, 'is_draft', False))

        # verify verticals are children of sequential
        for vert in [vertical, private_vertical, public_vertical]:
            self.assertIn(vert.location, sequential.children)

        # verify draft html is the child of the public vertical
        self.assertIn(draft_html.location, public_vertical.children)

        # verify draft video is the child of the public vertical
        self.assertIn(draft_video.location, public_vertical.children)

        # verify textbook exists
        course = self.store.get_course(course_id)
        self.assertGreater(len(course.textbooks), 0)

        # verify asset attributes of locked asset key
        self.assertAssetsEqual(self.LOCKED_ASSET_KEY,
                               self.LOCKED_ASSET_KEY.course_key, course_id)

        # verify non-portable links are rewritten
        html_module = self.store.get_item(
            course_id.make_usage_key('html', 'nonportable'))
        self.assertIn('/static/foo.jpg', html_module.data)

        return course

    def assertCoursesEqual(self, course1_id, course2_id):
        """
        Verifies the content of the two given courses are equal
        """
        course1_items = self.store.get_items(course1_id)
        course2_items = self.store.get_items(course2_id)
        self.assertGreater(len(course1_items),
                           0)  # ensure it found content instead of [] == []
        if len(course1_items) != len(course2_items):
            course1_block_ids = set(
                [item.location.block_id for item in course1_items])
            course2_block_ids = set(
                [item.location.block_id for item in course2_items])
            raise AssertionError(
                u"Course1 extra blocks: {}; course2 extra blocks: {}".format(
                    course1_block_ids - course2_block_ids,
                    course2_block_ids - course1_block_ids))

        for course1_item in course1_items:
            course1_item_loc = course1_item.location
            course2_item_loc = course2_id.make_usage_key(
                course1_item_loc.block_type, course1_item_loc.block_id)
            if course1_item_loc.block_type == 'course':
                # mongo uses the run as the name, split uses 'course'
                store = self.store._get_modulestore_for_courselike(course2_id)  # pylint: disable=protected-access
                new_name = 'course' if isinstance(
                    store, SplitMongoModuleStore) else course2_item_loc.run
                course2_item_loc = course2_item_loc.replace(name=new_name)
            course2_item = self.store.get_item(course2_item_loc)

            # compare published state
            self.assertEqual(self.store.has_published_version(course1_item),
                             self.store.has_published_version(course2_item))

            # compare data
            self.assertEqual(hasattr(course1_item, 'data'),
                             hasattr(course2_item, 'data'))
            if hasattr(course1_item, 'data'):
                self.assertEqual(course1_item.data, course2_item.data)

            # compare meta-data
            course1_metadata = own_metadata(course1_item)
            course2_metadata = own_metadata(course2_item)
            # Omit edx_video_id as it can be different in case of extrnal video imports.
            course1_metadata.pop('edx_video_id', None)
            course2_metadata.pop('edx_video_id', None)
            self.assertEqual(course1_metadata, course2_metadata)

            # compare children
            self.assertEqual(course1_item.has_children,
                             course2_item.has_children)
            if course1_item.has_children:
                expected_children = []
                for course1_item_child in course1_item.children:
                    expected_children.append(
                        course2_id.make_usage_key(
                            course1_item_child.block_type,
                            course1_item_child.block_id))
                self.assertEqual(expected_children, course2_item.children)

        # compare assets
        content_store = self.store.contentstore
        course1_assets, count_course1_assets = content_store.get_all_content_for_course(
            course1_id)
        _, count_course2_assets = content_store.get_all_content_for_course(
            course2_id)
        self.assertEqual(count_course1_assets, count_course2_assets)
        for asset in course1_assets:
            asset_son = asset.get('content_son', asset['_id'])
            self.assertAssetsEqual(asset_son, course1_id, course2_id)

    def check_verticals(self, items):
        """ Test getting the editing HTML for each vertical. """
        # assert is here to make sure that the course being tested actually has verticals (units) to check.
        self.assertGreater(len(items), 0,
                           "Course has no verticals (units) to check")
        for descriptor in items:
            resp = self.client.get_html(
                get_url('container_handler', descriptor.location))
            self.assertEqual(resp.status_code, 200)

    def assertAssetsEqual(self, asset_son, course1_id, course2_id):
        """Verifies the asset of the given key has the same attributes in both given courses."""
        content_store = contentstore()
        category = asset_son.block_type if hasattr(
            asset_son, 'block_type') else asset_son['category']
        filename = asset_son.block_id if hasattr(
            asset_son, 'block_id') else asset_son['name']
        course1_asset_attrs = content_store.get_attrs(
            course1_id.make_asset_key(category, filename))
        course2_asset_attrs = content_store.get_attrs(
            course2_id.make_asset_key(category, filename))
        self.assertEqual(len(course1_asset_attrs), len(course2_asset_attrs))
        for key, value in six.iteritems(course1_asset_attrs):
            if key in [
                    '_id', 'filename', 'uploadDate', 'content_son',
                    'thumbnail_location'
            ]:
                pass
            else:
                self.assertEqual(value, course2_asset_attrs[key])
Пример #40
0
 def test_asset_with_missing_parts(self, path):
     with self.assertRaises(InvalidKeyError):
         AssetKey.from_string(path)
Пример #41
0
 def test_asset_with_special_character(self, path):
     asset_locator = AssetKey.from_string(path)
     self.assertEqual(
         path,
         text_type(asset_locator),
     )
Пример #42
0
 def test_deprecated_round_trip_asset_location(self, path):
     self.assertEquals(
         path,
         unicode(AssetKey.from_string(path)),
     )
Пример #43
0
class TestAssetLocators(TestCase):
    """
    Tests of :class:`AssetLocator`
    """
    @ddt.data(
        "/c4x/org/course/asset/path", )
    def test_deprecated_round_trip_asset_location(self, path):
        self.assertEquals(
            path,
            unicode(AssetKey.from_string(path)),
        )

    def test_map_into_course_asset_location(self):
        original_course = CourseKey.from_string('org/course/run')
        new_course = CourseKey.from_string('edX/toy/2012_Fall')
        loc = AssetLocator(original_course, 'asset', 'foo.bar')
        self.assertEquals(
            AssetLocator(new_course, 'asset', 'foo.bar', deprecated=True),
            loc.map_into_course(new_course))

    def test_make_asset_key(self):
        course = CourseKey.from_string('org/course/run')
        self.assertEquals(
            AssetLocator(course, 'asset', 'foo.bar', deprecated=True),
            course.make_asset_key('asset', 'foo.bar'))

    @ddt.data(
        (AssetLocator, '_id.', 'c4x', (CourseLocator(
            'org', 'course', 'run', 'rev', deprecated=True), 'ct', 'n')),
        (AssetLocator, '_id.', 'c4x', (CourseLocator(
            'org', 'course', 'run', 'rev', deprecated=True), 'ct', None)),
    )
    @ddt.unpack
    def test_deprecated_son(self, key_cls, prefix, tag, source):
        source_key = key_cls(*source, deprecated=True)
        son = source_key.to_deprecated_son(prefix=prefix, tag=tag)
        self.assertEquals(son.keys(), [
            prefix + key
            for key in ('tag', 'org', 'course', 'category', 'name', 'revision')
        ])

        self.assertEquals(son[prefix + 'tag'], tag)
        self.assertEquals(son[prefix + 'category'], source_key.block_type)
        self.assertEquals(son[prefix + 'name'], source_key.block_id)

        self.assertEquals(son[prefix + 'org'], source_key.course_key.org)
        self.assertEquals(son[prefix + 'course'], source_key.course_key.course)
        self.assertEquals(son[prefix + 'revision'],
                          source_key.course_key.branch)

    @ddt.data(
        (AssetKey.from_string('/c4x/o/c/ct/n'), 'run'),
        (AssetKey.from_string('/c4x/o/c/ct/n@v'), 'run'),
    )
    @ddt.unpack
    def test_roundtrip_deprecated_son(self, key, run):
        self.assertEquals(
            key.replace(course_key=key.course_key.replace(run=run)),
            key.__class__._from_deprecated_son(key.to_deprecated_son(), run)  # pylint: disable=protected-access
        )

    def test_replace(self):
        asset_key = AssetKey.from_string('/c4x/o/c/asset/path')
        self.assertEquals('foo', asset_key.replace(path='foo').path)
        self.assertEquals('bar',
                          asset_key.replace(asset_type='bar').asset_type)

    def test_empty_path(self):
        with self.assertRaises(InvalidKeyError):
            CourseKey.from_string('course-v1:org+course+run').make_asset_key(
                'asset', '')

        self.assertEquals(
            '/c4x/org/course/asset/',
            unicode(
                CourseKey.from_string('org/course/run').make_asset_key(
                    'asset', '')))
Пример #44
0
 def test_asset_with_missing_parts(self, path):
     with self.assertRaises(InvalidKeyError):
         AssetKey.from_string(path)
Пример #45
0
class TestAssetLocators(TestCase):
    """
    Tests of :class:`AssetLocator`
    """
    @ddt.data(
        "/c4x/org/course/asset/path", )
    def test_deprecated_round_trip_asset_location(self, path):
        self.assertEqual(
            path,
            str(AssetKey.from_string(path)),
        )

    def test_map_into_course_asset_location(self):
        original_course = CourseKey.from_string('org/course/run')
        new_course = CourseKey.from_string('edX/toy/2012_Fall')
        loc = AssetLocator(original_course, 'asset', 'foo.bar')
        self.assertEqual(
            AssetLocator(new_course, 'asset', 'foo.bar', deprecated=True),
            loc.map_into_course(new_course))

    def test_make_asset_key(self):
        course = CourseKey.from_string('org/course/run')
        self.assertEqual(
            AssetLocator(course, 'asset', 'foo.bar', deprecated=True),
            course.make_asset_key('asset', 'foo.bar'))

    @ddt.data(
        (AssetLocator, '_id.', 'c4x', (CourseLocator(
            'org', 'course', 'run', 'rev', deprecated=True), 'ct', 'n')),
        (AssetLocator, '_id.', 'c4x', (CourseLocator(
            'org', 'course', 'run', 'rev', deprecated=True), 'ct', None)),
    )
    @ddt.unpack
    def test_deprecated_son(self, key_cls, prefix, tag, source):
        source_key = key_cls(*source, deprecated=True)
        son = source_key.to_deprecated_son(prefix=prefix, tag=tag)
        self.assertEqual(son.keys(), [
            prefix + key
            for key in ('tag', 'org', 'course', 'category', 'name', 'revision')
        ])

        self.assertEqual(son[prefix + 'tag'], tag)
        self.assertEqual(son[prefix + 'category'], source_key.block_type)
        self.assertEqual(son[prefix + 'name'], source_key.block_id)

        self.assertEqual(son[prefix + 'org'], source_key.course_key.org)
        self.assertEqual(son[prefix + 'course'], source_key.course_key.course)
        self.assertEqual(son[prefix + 'revision'],
                         source_key.course_key.branch)

    @ddt.data(
        (AssetKey.from_string('/c4x/o/c/ct/n'), 'run'),
        (AssetKey.from_string('/c4x/o/c/ct/n@v'), 'run'),
    )
    @ddt.unpack
    def test_roundtrip_deprecated_son(self, key, run):
        self.assertEqual(
            key.replace(course_key=key.course_key.replace(run=run)),
            key.__class__._from_deprecated_son(key.to_deprecated_son(), run)  # pylint: disable=protected-access
        )

    def test_old_charset(self):
        # merely not raising InvalidKeyError suffices
        AssetLocator(CourseLocator('a', 'b', 'c'), 'asset',
                     'subs_%20S2x5jhbWl_o.srt.sjson')
        AssetLocator(CourseLocator('a', 'b', 'c'),
                     'asset',
                     'subs_%20S2x5jhbWl_o.srt.sjson',
                     deprecated=True)

    def test_replace(self):
        asset_key = AssetKey.from_string('/c4x/o/c/asset/path')
        self.assertEqual('foo', asset_key.replace(path='foo').path)
        self.assertEqual('bar', asset_key.replace(asset_type='bar').asset_type)

    def test_empty_path(self):
        """ Test InvalidKeyError when asset path is empty """
        with self.assertRaises(InvalidKeyError):
            CourseKey.from_string(
                'course-locator:org+course+run').make_asset_key('asset', '')

        with self.assertRaises(InvalidKeyError):
            CourseKey.from_string('org/course/run').make_asset_key('asset', '')

    @ddt.data(
        "asset-v1:UOG+cs_34+Cs128+type@asset+block@subs_Introduction%20To%20New.srt.sjson",
        "asset-v1:UOG+cs_34+Cs128+type@asset+block@subs_Introduction~To~New.srt.sjson",
        "asset-v1:UOG+cs_34+Cs128+type@asset+block@subs_Introduction:To:New.srt.sjson",
        "asset-v1:UOG+cs_34+Cs128+type@asset+block@subs_Introduction-To-New.srt.sjson",
    )
    def test_asset_with_special_character(self, path):
        asset_locator = AssetKey.from_string(path)
        self.assertEqual(
            path,
            str(asset_locator),
        )

    @ddt.data(*itertools.product(
        ("asset-v1:UOG+cs_34+Cs128+type@asset+block@subs_Introduction_To_New.srt.sjson{}",
         ),
        ('\n', '\n\n', ' ', '   ', '   \n'),
    ))
    @ddt.unpack
    def test_asset_with_trailing_whitespace(self, path_fmt, whitespace):
        with self.assertRaises(InvalidKeyError):
            AssetKey.from_string(path_fmt.format(whitespace))

    @ddt.data(
        "asset-v1:UOG+cs_34+Cs128+type@asset",
        "asset-v1:UOG+cs_34+Cs128",
    )
    def test_asset_with_missing_parts(self, path):
        with self.assertRaises(InvalidKeyError):
            AssetKey.from_string(path)
Пример #46
0
 def test_deprecated_round_trip_asset_location(self, path):
     self.assertEqual(
         path,
         str(AssetKey.from_string(path)),
     )
Пример #47
0
 def test_asset_with_trailing_whitespace(self, path_fmt, whitespace):
     with self.assertRaises(InvalidKeyError):
         AssetKey.from_string(path_fmt.format(whitespace))