Skip to content

Commit

Permalink
feat: add xblock api asset connection so that we can actually test this
Browse files Browse the repository at this point in the history
  • Loading branch information
ormsbee committed Oct 11, 2024
1 parent f112ab4 commit 40abbe5
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 18 deletions.
10 changes: 10 additions & 0 deletions cms/envs/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,13 @@
"SECRET": "***",
"URL": "***",
}

############## openedx-learning (Learning Core) config ##############
OPENEDX_LEARNING = {
'MEDIA': {
'BACKEND': 'django.core.files.storage.InMemoryStorage',
'OPTIONS': {
'location': MEDIA_ROOT + "_private"
}
}
}
117 changes: 109 additions & 8 deletions openedx/core/djangoapps/content_libraries/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import base64
import hashlib
import logging
import mimetypes

import attr
import requests
Expand All @@ -68,6 +69,7 @@
from django.db.models import Q, QuerySet
from django.utils.translation import gettext as _
from edx_rest_api_client.client import OAuthAPIClient
from django.urls import reverse
from lxml import etree
from opaque_keys.edx.keys import BlockTypeKey, UsageKey, UsageKeyV2
from opaque_keys.edx.locator import (
Expand Down Expand Up @@ -95,7 +97,11 @@
from xblock.core import XBlock
from xblock.exceptions import XBlockNotFoundError

from openedx.core.djangoapps.xblock.api import get_component_from_usage_key, xblock_type_display_name
from openedx.core.djangoapps.xblock.api import (
get_component_from_usage_key,
get_xblock_app_config,
xblock_type_display_name,
)
from openedx.core.lib.xblock_serializer.api import serialize_modulestore_block_for_learning_core
from xmodule.library_root_xblock import LibraryRoot as LibraryRootV1
from xmodule.modulestore import ModuleStoreEnum
Expand Down Expand Up @@ -1001,18 +1007,48 @@ def get_library_block_static_asset_files(usage_key) -> list[LibraryXBlockStaticF
Returns a list of LibraryXBlockStaticFile objects, sorted by path.
TODO: This is not yet implemented for Learning Core backed libraries.
TODO: Should this be in the general XBlock API rather than the libraries API?
"""
return []
component = get_component_from_usage_key(usage_key)
component_version = component.versioning.draft

# If there is no Draft version, then this was soft-deleted
if component_version is None:
return []

# cvc = the ComponentVersionContent through table
cvc_set = (
component_version
.componentversioncontent_set
.filter(content__has_file=True)
.order_by('key')
.select_related('content')
)

site_root_url = get_xblock_app_config().get_site_root_url()

return [
LibraryXBlockStaticFile(
path=cvc.key,
size=cvc.content.size,
url=site_root_url + reverse(
'content_libraries:library-assets',
kwargs={
'component_version_uuid': component_version.uuid,
'asset_path': cvc.key,
}
),
)
for cvc in cvc_set
]


def add_library_block_static_asset_file(usage_key, file_name, file_content) -> LibraryXBlockStaticFile:
def add_library_block_static_asset_file(usage_key, file_path, file_content, user=None) -> LibraryXBlockStaticFile:
"""
Upload a static asset file into the library, to be associated with the
specified XBlock. Will silently overwrite an existing file of the same name.
file_name should be a name like "doc.pdf". It may optionally contain slashes
file_path should be a name like "doc.pdf". It may optionally contain slashes
like 'en/doc.pdf'
file_content should be a binary string.
Expand All @@ -1024,10 +1060,58 @@ def add_library_block_static_asset_file(usage_key, file_name, file_content) -> L
video_block = UsageKey.from_string("lb:VideoTeam:python-intro:video:1")
add_library_block_static_asset_file(video_block, "subtitles-en.srt", subtitles.encode('utf-8'))
"""
raise NotImplementedError("Static assets not yet implemented for Learning Core")
# File path validations copied over from v1 library logic...
if file_path != file_path.strip().strip('/'):
raise InvalidNameError("file_path cannot start/end with / or whitespace.")
if '//' in file_path or '..' in file_path:
raise InvalidNameError("Invalid sequence (// or ..) in file_path.")

component = get_component_from_usage_key(usage_key)

def delete_library_block_static_asset_file(usage_key, file_name):
media_type_str, _encoding = mimetypes.guess_type(file_path)
media_type = authoring_api.get_or_create_media_type(media_type_str)
now = datetime.now(tz=timezone.utc)

with transaction.atomic():
content = authoring_api.get_or_create_file_content(
component.publishable_entity.learning_package.id,
media_type.id,
data=file_content,
created=now,
)
component_version = authoring_api.create_next_component_version(
component.pk,
content_to_replace={file_path: content.id},
created=now,
created_by=user.id if user else None,
)
transaction.on_commit(
lambda: LIBRARY_BLOCK_UPDATED.send_event(
library_block=LibraryBlockData(
library_key=usage_key.context_key,
usage_key=usage_key,
)
)
)

# Now figure out the URL for the newly created asset...
site_root_url = get_xblock_app_config().get_site_root_url()
local_path = reverse(
'content_libraries:library-assets',
kwargs={
'component_version_uuid': component_version.uuid,
'asset_path': file_path,
}
)

return LibraryXBlockStaticFile(
path=file_path,
url=site_root_url + local_path,
size=content.size,
)


def delete_library_block_static_asset_file(usage_key, file_path, user=None):
"""
Delete a static asset file from the library.
Expand All @@ -1037,7 +1121,24 @@ def delete_library_block_static_asset_file(usage_key, file_name):
video_block = UsageKey.from_string("lb:VideoTeam:python-intro:video:1")
delete_library_block_static_asset_file(video_block, "subtitles-en.srt")
"""
raise NotImplementedError("Static assets not yet implemented for Learning Core")
component = get_component_from_usage_key(usage_key)
now = datetime.now(tz=timezone.utc)

with transaction.atomic():
component_version = authoring_api.create_next_component_version(
component.pk,
content_to_replace={file_path: None},
created=now,
created_by=user.id if user else None,
)
transaction.on_commit(
lambda: LIBRARY_BLOCK_UPDATED.send_event(
library_block=LibraryBlockData(
library_key=usage_key.context_key,
usage_key=usage_key,
)
)
)


def get_allowed_block_types(library_key): # pylint: disable=unused-argument
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,9 @@
"""


@skip("Assets are being reimplemented in Learning Core. Disable until that's ready.")
class ContentLibrariesStaticAssetsTest(ContentLibrariesRestApiTest):
"""
Tests for static asset files in Learning-Core-based Content Libraries
WARNING: every test should have a unique library slug, because even though
the django/mysql database gets reset for each test case, the lookup between
library slug and bundle UUID does not because it's assumed to be immutable
and cached forever.
"""

def test_asset_filenames(self):
Expand Down
10 changes: 6 additions & 4 deletions openedx/core/djangoapps/content_libraries/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from django.conf import settings
from django.contrib.auth import authenticate, get_user_model, login
from django.contrib.auth.models import Group
from django.core.exceptions import ObjectDoesNotExist
from django.db.transaction import atomic, non_atomic_requests
from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse, StreamingHttpResponse
from django.shortcuts import get_object_or_404
Expand All @@ -87,6 +88,7 @@
import edx_api_doc_tools as apidocs
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2
from openedx_learning.api import authoring
from organizations.api import ensure_organization
from organizations.exceptions import InvalidOrganizationException
from organizations.models import Organization
Expand Down Expand Up @@ -757,7 +759,7 @@ def put(self, request, usage_key_str, file_path):
raise ValidationError("File too big")
file_content = file_wrapper.read()
try:
result = api.add_library_block_static_asset_file(usage_key, file_path, file_content)
result = api.add_library_block_static_asset_file(usage_key, file_path, file_content, request.user)
except ValueError:
raise ValidationError("Invalid file path") # lint-amnesty, pylint: disable=raise-missing-from
return Response(LibraryXBlockStaticFileSerializer(result).data)
Expand All @@ -772,7 +774,7 @@ def delete(self, request, usage_key_str, file_path):
usage_key.lib_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY,
)
try:
api.delete_library_block_static_asset_file(usage_key, file_path)
api.delete_library_block_static_asset_file(usage_key, file_path, request.user)
except ValueError:
raise ValidationError("Invalid file path") # lint-amnesty, pylint: disable=raise-missing-from
return Response(status=status.HTTP_204_NO_CONTENT)
Expand Down Expand Up @@ -1125,7 +1127,7 @@ def component_version_asset(request, component_version_uuid, asset_path):
eventually).
"""
try:
component_version = authoring_api.get_component_version_by_uuid(
component_version = authoring.get_component_version_by_uuid(
component_version_uuid
)
except ObjectDoesNotExist:
Expand All @@ -1144,7 +1146,7 @@ def component_version_asset(request, component_version_uuid, asset_path):
# this response in conjunction with a media reverse proxy (Caddy or Nginx),
# but in the short term we're just going to remove the redirect and stream
# the content directly.
redirect_response = authoring_api.get_redirect_response_for_component_asset(
redirect_response = authoring.get_redirect_response_for_component_asset(
component_version_uuid,
asset_path,
public=False,
Expand Down

0 comments on commit 40abbe5

Please sign in to comment.