diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index 7c305599253..b45ea81e148 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -1073,6 +1073,11 @@ def add_library_block_static_asset_file(usage_key, file_path, file_content, user component = get_component_from_usage_key(usage_key) media_type_str, _encoding = mimetypes.guess_type(file_path) + # We use "application/octet-stream" as a generic fallback media type, per + # RFC 2046: https://datatracker.ietf.org/doc/html/rfc2046 + # TODO: This probably makes sense to push down to openedx-learning? + media_type_str = media_type_str or "application/octet-stream" + now = datetime.now(tz=timezone.utc) with transaction.atomic(): diff --git a/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py b/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py index e2cc2bc3c30..f34d1e87b9e 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py @@ -1,11 +1,16 @@ """ Tests for static asset files in Learning-Core-based Content Libraries """ +from uuid import UUID from unittest import skip +from opaque_keys.edx.keys import UsageKey + from openedx.core.djangoapps.content_libraries.tests.base import ( ContentLibrariesRestApiTest, ) +from openedx.core.djangoapps.xblock.api import get_component_from_usage_key +from openedx.core.djangolib.testing.utils import skip_unless_cms # Binary data representing an SVG image file SVG_DATA = """ @@ -22,7 +27,7 @@ I'm Anant Agarwal, I'm the president of edX, """ - +@skip_unless_cms class ContentLibrariesStaticAssetsTest(ContentLibrariesRestApiTest): """ Tests for static asset files in Learning-Core-based Content Libraries @@ -102,3 +107,66 @@ def check_download(): self._commit_library_changes(library["id"]) check_sjson() check_download() + + +@skip_unless_cms +class ContentLibrariesComponentVersionAssetTest(ContentLibrariesRestApiTest): + """ + Tests for the view that actually delivers the Library asset in Studio. + """ + + def setUp(self): + super().setUp() + + library = self._create_library(slug="asset-lib2", title="Static Assets Test Library") + block = self._add_block_to_library(library["id"], "html", "html1") + self._set_library_block_asset(block["id"], "static/test.svg", SVG_DATA) + usage_key = UsageKey.from_string(block["id"]) + self.component = get_component_from_usage_key(usage_key) + self.draft_component_version = self.component.versioning.draft + + + def test_good_responses(self): + get_response = self.client.get( + f"/library_assets/{self.draft_component_version.uuid}/static/test.svg" + ) + assert get_response.status_code == 200 + content = b''.join(chunk for chunk in get_response.streaming_content) + assert content == SVG_DATA + + good_head_response = self.client.head( + f"/library_assets/{self.draft_component_version.uuid}/static/test.svg" + ) + assert good_head_response.headers == get_response.headers + + + def test_missing(self): + """Test asset requests that should 404.""" + # Non-existent version... + wrong_version_uuid = UUID('11111111-1111-1111-1111-111111111111') + response = self.client.get( + f"/library_assets/{wrong_version_uuid}/static/test.svg" + ) + assert response.status_code == 404 + + # Non-existent file... + response = self.client.get( + f"/library_assets/{self.draft_component_version.uuid}/static/missing.svg" + ) + assert response.status_code == 404 + + # File-like ComponenVersionContent entry that isn't an actually + # downloadable file... + response = self.client.get( + f"/library_assets/{self.draft_component_version.uuid}/block.xml" + ) + assert response.status_code == 404 + + + def test_anonymous_user(self): + """Anonymous users shouldn't get access to library assets.""" + self.client.logout() + response = self.client.get( + f"/library_assets/{self.draft_component_version.uuid}/static/test.svg" + ) + assert response.status_code == 403 diff --git a/xmodule/video_block/transcripts_utils.py b/xmodule/video_block/transcripts_utils.py index 12a629501d9..4e468c7052e 100644 --- a/xmodule/video_block/transcripts_utils.py +++ b/xmodule/video_block/transcripts_utils.py @@ -15,6 +15,7 @@ import requests import simplejson as json from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist from lxml import etree from opaque_keys.edx.keys import UsageKeyV2 from openedx_learning.api import authoring @@ -1098,27 +1099,34 @@ def get_transcript_from_learning_core(video_block, language, output_format, tran component_version = component.versioning.draft if not component_version: raise NotFoundError( - f"No transcript for {usage_key}: Component {component.uuid} was soft-deleted." + f"No transcript for {usage_key} because Component {component.uuid} " + "was soft-deleted." ) file_path = pathlib.Path(f"static/{transcripts[language]}") if file_path.suffix != '.srt': # We want to standardize on .srt - raise NotFoundError("Video XBlocks in Content Libraries only support .srt transcript files.") + raise NotFoundError( + "Video XBlocks in Content Libraries only support storing .srt " + f"transcript files, but we tried to look up {path_file} for {usage_key}" + ) # TODO: There should be a Learning Core API call for this: - print( - [(cvc.key, cvc.content.has_file) for cvc in component_version.componentversioncontent_set.all()] - ) - content = ( - component_version - .componentversioncontent_set - .filter(content__has_file=True) - .select_related('content') - .get(key=file_path) - .content - ) - data = content.read_file().read() + try: + content = ( + component_version + .componentversioncontent_set + .filter(content__has_file=True) + .select_related('content') + .get(key=file_path) + .content + ) + data = content.read_file().read() + except ObjectDoesNotExist: + raise NotFoundError( + f"No file {file_path} found for {usage_key} " + f"(ComponentVersion {component_version.uuid})" + ) # Now convert the transcript data to the requested format: output_filename = f'{file_path.stem}.{output_format}' @@ -1128,7 +1136,11 @@ def get_transcript_from_learning_core(video_block, language, output_format, tran output_format=output_format, ) if not output_transcript.strip(): - raise NotFoundError('No transcript content') + raise NotFoundError( + f"Transcript file {file_path} found for {usage_key} " + f"(ComponentVersion {component_version.uuid}), but it has no " + "content or is malformed." + ) return output_transcript, output_filename, Transcript.mime_types[output_format]