56
56
import base64
57
57
import hashlib
58
58
import logging
59
+ import mimetypes
59
60
60
61
import attr
61
62
import requests
68
69
from django .db .models import Q , QuerySet
69
70
from django .utils .translation import gettext as _
70
71
from edx_rest_api_client .client import OAuthAPIClient
72
+ from django .urls import reverse
71
73
from lxml import etree
72
74
from opaque_keys .edx .keys import BlockTypeKey , UsageKey , UsageKeyV2
73
75
from opaque_keys .edx .locator import (
96
98
from xblock .core import XBlock
97
99
from xblock .exceptions import XBlockNotFoundError
98
100
99
- from openedx .core .djangoapps .xblock .api import get_component_from_usage_key , xblock_type_display_name
101
+ from openedx .core .djangoapps .xblock .api import (
102
+ get_component_from_usage_key ,
103
+ get_xblock_app_config ,
104
+ xblock_type_display_name ,
105
+ )
100
106
from openedx .core .lib .xblock_serializer .api import serialize_modulestore_block_for_learning_core
101
107
from xmodule .modulestore .django import modulestore
102
108
@@ -1018,18 +1024,48 @@ def get_library_block_static_asset_files(usage_key) -> list[LibraryXBlockStaticF
1018
1024
1019
1025
Returns a list of LibraryXBlockStaticFile objects, sorted by path.
1020
1026
1021
- TODO: This is not yet implemented for Learning Core backed libraries.
1022
1027
TODO: Should this be in the general XBlock API rather than the libraries API?
1023
1028
"""
1024
- return []
1029
+ component = get_component_from_usage_key (usage_key )
1030
+ component_version = component .versioning .draft
1031
+
1032
+ # If there is no Draft version, then this was soft-deleted
1033
+ if component_version is None :
1034
+ return []
1035
+
1036
+ # cvc = the ComponentVersionContent through table
1037
+ cvc_set = (
1038
+ component_version
1039
+ .componentversioncontent_set
1040
+ .filter (content__has_file = True )
1041
+ .order_by ('key' )
1042
+ .select_related ('content' )
1043
+ )
1044
+
1045
+ site_root_url = get_xblock_app_config ().get_site_root_url ()
1046
+
1047
+ return [
1048
+ LibraryXBlockStaticFile (
1049
+ path = cvc .key ,
1050
+ size = cvc .content .size ,
1051
+ url = site_root_url + reverse (
1052
+ 'content_libraries:library-assets' ,
1053
+ kwargs = {
1054
+ 'component_version_uuid' : component_version .uuid ,
1055
+ 'asset_path' : cvc .key ,
1056
+ }
1057
+ ),
1058
+ )
1059
+ for cvc in cvc_set
1060
+ ]
1025
1061
1026
1062
1027
- def add_library_block_static_asset_file (usage_key , file_name , file_content ) -> LibraryXBlockStaticFile :
1063
+ def add_library_block_static_asset_file (usage_key , file_path , file_content , user = None ) -> LibraryXBlockStaticFile :
1028
1064
"""
1029
1065
Upload a static asset file into the library, to be associated with the
1030
1066
specified XBlock. Will silently overwrite an existing file of the same name.
1031
1067
1032
- file_name should be a name like "doc.pdf". It may optionally contain slashes
1068
+ file_path should be a name like "doc.pdf". It may optionally contain slashes
1033
1069
like 'en/doc.pdf'
1034
1070
file_content should be a binary string.
1035
1071
@@ -1041,10 +1077,67 @@ def add_library_block_static_asset_file(usage_key, file_name, file_content) -> L
1041
1077
video_block = UsageKey.from_string("lb:VideoTeam:python-intro:video:1")
1042
1078
add_library_block_static_asset_file(video_block, "subtitles-en.srt", subtitles.encode('utf-8'))
1043
1079
"""
1044
- raise NotImplementedError ("Static assets not yet implemented for Learning Core" )
1080
+ # File path validations copied over from v1 library logic. This can't really
1081
+ # hurt us inside our system because we never use these paths in an actual
1082
+ # file system–they're just string keys that point to hash-named data files
1083
+ # in a common library (learning package) level directory. But it might
1084
+ # become a security issue during import/export serialization.
1085
+ if file_path != file_path .strip ().strip ('/' ):
1086
+ raise InvalidNameError ("file_path cannot start/end with / or whitespace." )
1087
+ if '//' in file_path or '..' in file_path :
1088
+ raise InvalidNameError ("Invalid sequence (// or ..) in file_path." )
1089
+
1090
+ component = get_component_from_usage_key (usage_key )
1091
+
1092
+ media_type_str , _encoding = mimetypes .guess_type (file_path )
1093
+ # We use "application/octet-stream" as a generic fallback media type, per
1094
+ # RFC 2046: https://datatracker.ietf.org/doc/html/rfc2046
1095
+ # TODO: This probably makes sense to push down to openedx-learning?
1096
+ media_type_str = media_type_str or "application/octet-stream"
1097
+
1098
+ now = datetime .now (tz = timezone .utc )
1099
+
1100
+ with transaction .atomic ():
1101
+ media_type = authoring_api .get_or_create_media_type (media_type_str )
1102
+ content = authoring_api .get_or_create_file_content (
1103
+ component .publishable_entity .learning_package .id ,
1104
+ media_type .id ,
1105
+ data = file_content ,
1106
+ created = now ,
1107
+ )
1108
+ component_version = authoring_api .create_next_component_version (
1109
+ component .pk ,
1110
+ content_to_replace = {file_path : content .id },
1111
+ created = now ,
1112
+ created_by = user .id if user else None ,
1113
+ )
1114
+ transaction .on_commit (
1115
+ lambda : LIBRARY_BLOCK_UPDATED .send_event (
1116
+ library_block = LibraryBlockData (
1117
+ library_key = usage_key .context_key ,
1118
+ usage_key = usage_key ,
1119
+ )
1120
+ )
1121
+ )
1045
1122
1123
+ # Now figure out the URL for the newly created asset...
1124
+ site_root_url = get_xblock_app_config ().get_site_root_url ()
1125
+ local_path = reverse (
1126
+ 'content_libraries:library-assets' ,
1127
+ kwargs = {
1128
+ 'component_version_uuid' : component_version .uuid ,
1129
+ 'asset_path' : file_path ,
1130
+ }
1131
+ )
1132
+
1133
+ return LibraryXBlockStaticFile (
1134
+ path = file_path ,
1135
+ url = site_root_url + local_path ,
1136
+ size = content .size ,
1137
+ )
1046
1138
1047
- def delete_library_block_static_asset_file (usage_key , file_name ):
1139
+
1140
+ def delete_library_block_static_asset_file (usage_key , file_path , user = None ):
1048
1141
"""
1049
1142
Delete a static asset file from the library.
1050
1143
@@ -1054,7 +1147,24 @@ def delete_library_block_static_asset_file(usage_key, file_name):
1054
1147
video_block = UsageKey.from_string("lb:VideoTeam:python-intro:video:1")
1055
1148
delete_library_block_static_asset_file(video_block, "subtitles-en.srt")
1056
1149
"""
1057
- raise NotImplementedError ("Static assets not yet implemented for Learning Core" )
1150
+ component = get_component_from_usage_key (usage_key )
1151
+ now = datetime .now (tz = timezone .utc )
1152
+
1153
+ with transaction .atomic ():
1154
+ component_version = authoring_api .create_next_component_version (
1155
+ component .pk ,
1156
+ content_to_replace = {file_path : None },
1157
+ created = now ,
1158
+ created_by = user .id if user else None ,
1159
+ )
1160
+ transaction .on_commit (
1161
+ lambda : LIBRARY_BLOCK_UPDATED .send_event (
1162
+ library_block = LibraryBlockData (
1163
+ library_key = usage_key .context_key ,
1164
+ usage_key = usage_key ,
1165
+ )
1166
+ )
1167
+ )
1058
1168
1059
1169
1060
1170
def get_allowed_block_types (library_key ): # pylint: disable=unused-argument
0 commit comments