Skip to content

Commit

Permalink
feat: very hacky first pass at getting static assets to show in v2 libs
Browse files Browse the repository at this point in the history
  • Loading branch information
ormsbee committed Sep 27, 2024
1 parent 75d111a commit c2e1d24
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 9 deletions.
1 change: 0 additions & 1 deletion common/djangoapps/static_replace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ def replace_static_urls(
xblock: xblock where the static assets are stored
lookup_url_func: Lookup function which returns the correct path of the asset
"""

if static_paths_out is None:
static_paths_out = []

Expand Down
1 change: 1 addition & 0 deletions common/djangoapps/static_replace/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def replace_urls(self, text, static_replace_only=False):
static_replace_only: If True, only static urls will be replaced
"""
block = self.xblock()

if self.lookup_asset_url:
text = replace_static_urls(text, xblock=block, lookup_asset_url=self.lookup_asset_url)
else:
Expand Down
9 changes: 9 additions & 0 deletions lms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,19 @@
),
]

from openedx.core.djangoapps.content_libraries.views import component_version_asset

urlpatterns = [
path('', branding_views.index, name='root'), # Main marketing page, or redirect to courseware

# this is definitely wrong for now, but I need it to test stuff end-to-end.
path(
'library_assets/<uuid:component_version_uuid>/<path:asset_path>',
component_version_asset,
name='library-assets',
),


path('', include('common.djangoapps.student.urls')),
# TODO: Move lms specific student views out of common code
re_path(r'^dashboard/?$', student_views.student_dashboard, name='dashboard'),
Expand Down
32 changes: 31 additions & 1 deletion openedx/core/djangoapps/content_libraries/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
from django.db import IntegrityError, transaction
from django.db.models import Q, QuerySet
from django.utils.translation import gettext as _
from django.urls import reverse
from edx_rest_api_client.client import OAuthAPIClient
from lxml import etree
from opaque_keys.edx.keys import BlockTypeKey, UsageKey, UsageKeyV2
Expand Down Expand Up @@ -997,7 +998,36 @@ def get_library_block_static_asset_files(usage_key) -> list[LibraryXBlockStaticF
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')
)

return [
LibraryXBlockStaticFile(
path=cvc.key,
size=cvc.content.size,
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:
Expand Down
5 changes: 5 additions & 0 deletions openedx/core/djangoapps/content_libraries/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,9 @@
path('pub/jwks/', views.LtiToolJwksView.as_view(), name='lti-pub-jwks'),
])),
])),
path(
'library_assets/<uuid:component_version_uuid>/<path:asset_path>',
views.component_version_asset,
name='library-assets',
),
]
72 changes: 71 additions & 1 deletion openedx/core/djangoapps/content_libraries/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,22 @@
from django.contrib.auth import authenticate, get_user_model, login
from django.contrib.auth.models import Group
from django.db.transaction import atomic, non_atomic_requests
from django.http import Http404, HttpResponseBadRequest, JsonResponse
from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse, StreamingHttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_safe
from django.views.generic.base import TemplateResponseMixin, View
from pylti1p3.contrib.django import DjangoCacheDataStorage, DjangoDbToolConf, DjangoMessageLaunch, DjangoOIDCLogin
from pylti1p3.exception import LtiException, OIDCException

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 as authoring_api
from organizations.api import ensure_organization
from organizations.exceptions import InvalidOrganizationException
from organizations.models import Organization
Expand Down Expand Up @@ -1107,3 +1109,71 @@ def get(self, request):
Return the JWKS.
"""
return JsonResponse(self.lti_tool_config.get_jwks(), safe=False)


@require_safe
def component_version_asset(request, component_version_uuid, asset_path):
"""
Serves static assets associated with particular Component versions.
Important notes:
* This is meant for Studio/authoring use ONLY. It requires read access to
the content library.
* It uses the UUID because that's easier to parse than the key field (which
could be part of an OpaqueKey, but could also be almost anything else).
* This is not very performant, and we still want to use the X-Accel-Redirect
method for serving LMS traffic in the longer term (and probably Studio
eventually).
"""
try:
component_version = authoring_api.get_component_version_by_uuid(
component_version_uuid
)
except ObjectDoesNotExist:
raise Http404()

learning_package = component_version.component.learning_package
library_key = LibraryLocatorV2.from_string(learning_package.key)

api.require_permission_for_library_key(
library_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY,
)

# We already have logic for getting the correct content and generating the
# proper headers in Learning Core, but the response generated here is an
# X-Accel-Redirect and lacks the actual content. We eventually want to use
# 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(
component_version_uuid,
asset_path,
public=False,
learner_downloadable_only=False,
)

# If there was any error, we return that response because it will have the
# correct headers set and won't have any X-Accel-Redirect header set.
if redirect_response.status_code != 200:
return redirect_response

cv_content = component_version.componentversioncontent_set.get(key=asset_path)
content = cv_content.content

# Delete the re-direct part of the response headers. We'll copy the rest.
headers = redirect_response.headers
headers.pop('X-Accel-Redirect')

# We need to set the content size header manually because this is a
# streaming response. It's not included in the redirect headers because it's
# not needed there (the reverse-proxy would have direct access to the file).
headers['Content-Length'] = content.size

if request.method == "HEAD":
return HttpResponse(headers=headers)

return StreamingHttpResponse(
content.read_file().chunks(),
headers=redirect_response.headers,
)
19 changes: 17 additions & 2 deletions openedx/core/djangoapps/xblock/runtime/learning_core_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from django.core.exceptions import ObjectDoesNotExist
from django.db.transaction import atomic
from django.urls import reverse

from openedx_learning.api import authoring as authoring_api

Expand Down Expand Up @@ -291,6 +292,20 @@ def _lookup_asset_url(self, block: XBlock, asset_path: str) -> str | None: # py
This is called by the XBlockRuntime superclass in the .runtime module.
TODO: Implement as part of larger static asset effort.
TODO: Like get_block, we currently assume that we're using the Draft
version. This should be a runtime parameter.
"""
return None
usage_key = block.scope_ids.usage_id
component = self._get_component_from_usage_key(usage_key)
component_version = component.versioning.draft
if component_version is None:
# This could happen if a Component was soft-deleted.
raise NoSuchUsage(usage_key)

return reverse(
'library-assets',
kwargs={
'component_version_uuid': component_version.uuid,
'asset_path': asset_path,
}
)
9 changes: 5 additions & 4 deletions openedx/core/lib/xblock_serializer/block_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ def __init__(self, block):
self.olx_str = etree.tostring(olx_node, encoding="unicode", pretty_print=True)

course_key = self.orig_block_key.course_key

# Search the OLX for references to files stored in the course's
# "Files & Uploads" (contentstore):
self.olx_str = utils.rewrite_absolute_static_urls(self.olx_str, course_key)
for asset in utils.collect_assets_from_text(self.olx_str, course_key):
path = asset['path']
if path not in [sf.name for sf in self.static_files]:
self.static_files.append(StaticFile(name=path, url=asset['url'], data=None))
# for asset in utils.collect_assets_from_text(self.olx_str, course_key):
# path = asset['path']
# if path not in [sf.name for sf in self.static_files]:
# self.static_files.append(StaticFile(name=path, url=asset['url'], data=None))

if block.scope_ids.usage_id.block_type in ['problem', 'vertical']:
py_lib_zip_file = utils.get_python_lib_zip_if_using(self.olx_str, course_key)
Expand Down

0 comments on commit c2e1d24

Please sign in to comment.