Skip to content
4 changes: 2 additions & 2 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ layers=
# has no child elements.
openedx_content.applets.components

# The "contents" applet stores the simplest pieces of binary and text data,
# The "media" applet stores the simplest pieces of binary and text data,
# without versioning information. These belong to a single Learning Package.
openedx_content.applets.contents
openedx_content.applets.media

# The "collections" applet stores arbitrary groupings of PublishableEntities.
# Its only dependency should be the publishing app.
Expand Down
10 changes: 5 additions & 5 deletions olx_importer/management/commands/load_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ def create_content(self, static_local_path, now, component_version):
logger.warning(f' Static reference not found: "{real_path}"')
return # Might as well bail if we can't find the file.

content = content_api.get_or_create_file_content(
media = content_api.get_or_create_file_media(
self.learning_package.id,
data=data_bytes,
mime_type=mime_type,
created=now,
)
content_api.create_component_version_content(
content_api.create_component_version_media(
component_version,
content.id,
media.id,
key=key,
)

Expand Down Expand Up @@ -163,14 +163,14 @@ def import_block_type(self, block_type_name, now): # , publish_log_entry):

# Create the Content entry for the raw data...
text = xml_file_path.read_text('utf-8')
text_content, _created = content_api.get_or_create_text_content(
text_content, _created = content_api.get_or_create_text_media(
self.learning_package.id,
text=text,
mime_type=f"application/vnd.openedx.xblock.v1.{block_type_name}+xml",
created=now,
)
# Add the OLX source text to the ComponentVersion
content_api.create_component_version_content(
content_api.create_component_version_media(
component_version,
text_content.pk,
key="block.xml",
Expand Down
2 changes: 1 addition & 1 deletion src/openedx_content/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .applets.backup_restore.admin import *
from .applets.collections.admin import *
from .applets.components.admin import *
from .applets.contents.admin import *
from .applets.media.admin import *
from .applets.publishing.admin import *
from .applets.sections.admin import *
from .applets.subsections.admin import *
Expand Down
2 changes: 1 addition & 1 deletion src/openedx_content/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .applets.backup_restore.api import *
from .applets.collections.api import *
from .applets.components.api import *
from .applets.contents.api import *
from .applets.media.api import *
from .applets.publishing.api import *
from .applets.sections.api import *
from .applets.subsections.api import *
Expand Down
76 changes: 38 additions & 38 deletions src/openedx_content/applets/backup_restore/zipper.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@
Collection,
ComponentType,
ComponentVersion,
ComponentVersionContent,
Content,
ComponentVersionMedia,
Media,
LearningPackage,
PublishableEntity,
PublishableEntityVersion,
)

from ..collections import api as collections_api
from ..components import api as components_api
from ..contents import api as contents_api
from ..media import api as media_api
from ..publishing import api as publishing_api
from ..sections import api as sections_api
from ..subsections import api as subsections_api
Expand Down Expand Up @@ -191,14 +191,14 @@ def get_publishable_entities(self) -> QuerySet[PublishableEntity]:
# especially with large libraries (up to 100K items),
# which is too large for this type of prefetch.
Prefetch(
"draft__version__componentversion__componentversioncontent_set",
queryset=ComponentVersionContent.objects.select_related("content"),
to_attr="prefetched_contents",
"draft__version__componentversion__componentversionmedia_set",
queryset=ComponentVersionMedia.objects.select_related("media"),
to_attr="prefetched_media",
),
Prefetch(
"published__version__componentversion__componentversioncontent_set",
queryset=ComponentVersionContent.objects.select_related("content"),
to_attr="prefetched_contents",
"published__version__componentversion__componentversionmedia_set",
queryset=ComponentVersionMedia.objects.select_related("media"),
to_attr="prefetched_media",
),
)
.order_by("key")
Expand Down Expand Up @@ -372,29 +372,29 @@ def create_zip(self, path: str) -> None:
# ------ COMPONENT STATIC CONTENT -------------
component_version: ComponentVersion = version.componentversion

# Get content data associated with this version
contents: QuerySet[
ComponentVersionContent
] = component_version.prefetched_contents # type: ignore[attr-defined]
# Get media data associated with this version
prefetched_media: QuerySet[
ComponentVersionMedia
] = component_version.prefetched_media # type: ignore[attr-defined]

for component_version_content in contents:
content: Content = component_version_content.content
for component_version_media in prefetched_media:
media: Media = component_version_media.media

# Important: The component_version_content.key contains implicitly
# Important: The component_version_media.key contains implicitly
# the file name and the file extension
file_path = version_folder / component_version_content.key
file_path = version_folder / component_version_media.key

if content.has_file and content.path:
if media.has_file and media.path:
# If has_file, we pull it from the file system
with content.read_file() as f:
with media.read_file() as f:
file_data = f.read()
elif not content.has_file and content.text:
# Otherwise, we use the text content as the file data
file_data = content.text
elif not media.has_file and media.text:
# Otherwise, we use the text media as the file data
file_data = media.text
else:
# If no file and no text, we skip this content
# If no file and no text, we skip this media
continue
self.add_file_to_zip(zipf, file_path, file_data, timestamp=content.created)
self.add_file_to_zip(zipf, file_path, file_data, timestamp=media.created)

# ------ COLLECTION SERIALIZATION -------------
collections = self.get_collections()
Expand Down Expand Up @@ -792,13 +792,13 @@ def _save_components(self, learning_package, components, component_static_files)
for valid_published in components.get("components_published", []):
entity_key = valid_published.pop("entity_key")
version_num = valid_published["version_num"] # Should exist, validated earlier
content_to_replace = self._resolve_static_files(version_num, entity_key, component_static_files)
media_to_replace = self._resolve_static_files(version_num, entity_key, component_static_files)
self.all_published_entities_versions.add(
(entity_key, version_num)
) # Track published version
components_api.create_next_component_version(
self.components_map_by_key[entity_key].publishable_entity.id,
content_to_replace=content_to_replace,
media_to_replace=media_to_replace,
force_version_num=valid_published.pop("version_num", None),
created_by=self.user_id,
**valid_published
Expand Down Expand Up @@ -876,14 +876,14 @@ def _save_draft_versions(self, components, containers, component_static_files):
version_num = valid_draft["version_num"] # Should exist, validated earlier
if self._is_version_already_exists(entity_key, version_num):
continue
content_to_replace = self._resolve_static_files(version_num, entity_key, component_static_files)
media_to_replace = self._resolve_static_files(version_num, entity_key, component_static_files)
components_api.create_next_component_version(
self.components_map_by_key[entity_key].publishable_entity.id,
content_to_replace=content_to_replace,
media_to_replace=media_to_replace,
force_version_num=valid_draft.pop("version_num", None),
# Drafts can diverge from published, so we allow ignoring previous content
# Drafts can diverge from published, so we allow ignoring previous media
# Use case: published v1 had files A, B; draft v2 only has file A
ignore_previous_content=True,
ignore_previous_media=True,
created_by=self.user_id,
**valid_draft
)
Expand Down Expand Up @@ -970,7 +970,7 @@ def _resolve_static_files(
entity_key: str,
static_files_map: dict[str, List[str]]
) -> dict[str, bytes | int]:
"""Resolve static file paths into their binary content."""
"""Resolve static file paths into their binary media content."""
resolved_files: dict[str, bytes | int] = {}

static_file_key = f"{entity_key}:v{num_version}" # e.g., "xblock.v1:html:my_component_123456:v1"
Expand All @@ -979,21 +979,21 @@ def _resolve_static_files(
for static_file in static_files:
local_key = static_file.split(f"v{num_version}/")[-1]
with self.zipf.open(static_file, "r") as f:
content_bytes = f.read()
media_bytes = f.read()
if local_key == "block.xml":
# Special handling for block.xml to ensure
# storing the value as a content instance
# storing the value as a media instance
if not self.learning_package_id:
raise ValueError("learning_package_id must be set before resolving static files.")
text_content = contents_api.get_or_create_text_content(
text_media = media_api.get_or_create_text_media(
self.learning_package_id,
contents_api.get_or_create_media_type(f"application/vnd.openedx.xblock.v1.{block_type}+xml").id,
text=content_bytes.decode("utf-8"),
media_api.get_or_create_media_type(f"application/vnd.openedx.xblock.v1.{block_type}+xml").id,
text=media_bytes.decode("utf-8"),
created=self.utc_now,
)
resolved_files[local_key] = text_content.id
resolved_files[local_key] = text_media.id
else:
resolved_files[local_key] = content_bytes
resolved_files[local_key] = media_bytes
return resolved_files

def _resolve_children(self, entity_data: dict[str, Any], lookup_map: dict[str, Any]) -> list[Any]:
Expand Down
42 changes: 21 additions & 21 deletions src/openedx_content/applets/components/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from openedx_django_lib.admin_utils import ReadOnlyModelAdmin

from .models import Component, ComponentVersion, ComponentVersionContent
from .models import Component, ComponentVersion, ComponentVersionMedia


class ComponentVersionInline(admin.TabularInline):
Expand Down Expand Up @@ -54,14 +54,14 @@ class ContentInline(admin.TabularInline):
"""
Django admin configuration for Content
"""
model = ComponentVersion.contents.through
model = ComponentVersion.media.through

def get_queryset(self, request):
queryset = super().get_queryset(request)
return queryset.select_related(
"content",
"content__learning_package",
"content__media_type",
"media",
"media__learning_package",
"media__media_type",
"component_version",
"component_version__publishable_entity_version",
"component_version__component",
Expand All @@ -74,22 +74,22 @@ def get_queryset(self, request):
"rendered_data",
]
readonly_fields = [
"content",
"media",
"key",
"format_size",
"rendered_data",
]
extra = 0

def has_file(self, cvc_obj):
return cvc_obj.content.has_file
def has_file(self, cvm_obj):
return cvm_obj.media.has_file

def rendered_data(self, cvc_obj):
return content_preview(cvc_obj)
def rendered_data(self, cvm_obj):
return media_preview(cvm_obj)

@admin.display(description="Size")
def format_size(self, cvc_obj):
return filesizeformat(cvc_obj.content.size)
def format_size(self, cvm_obj):
return filesizeformat(cvm_obj.media.size)


@admin.register(ComponentVersion)
Expand All @@ -103,7 +103,7 @@ class ComponentVersionAdmin(ReadOnlyModelAdmin):
"title",
"version_num",
"created",
"contents",
"media",
]
fields = [
"component",
Expand Down Expand Up @@ -134,26 +134,26 @@ def format_text_for_admin_display(text: str) -> SafeText:
)


def content_preview(cvc_obj: ComponentVersionContent) -> SafeText:
def media_preview(cvm_obj: ComponentVersionMedia) -> SafeText:
"""
Get the HTML to display a preview of the given ComponentVersionContent
Get the HTML to display a preview of the given ComponentVersionMedia
"""
content_obj = cvc_obj.content
media_obj = cvm_obj.media

if content_obj.media_type.type == "image":
if media_obj.media_type.type == "image":
# This base64 encoding looks really goofy and is bad for performance,
# but image previews in the admin are extremely useful, and this lets us
# have them without creating a separate view in Open edX Core. (Keep in
# mind that these assets are private, so they cannot be accessed via the
# MEDIA_URL like most Django uploaded assets.)
data = content_obj.read_file().read()
data = media_obj.read_file().read()
return format_html(
'<img src="data:{};base64, {}" style="max-width: 100%;" /><br><pre>{}</pre>',
content_obj.mime_type,
media_obj.mime_type,
base64.encodebytes(data).decode('utf8'),
content_obj.os_path(),
media_obj.os_path(),
)

return format_text_for_admin_display(
content_obj.text or ""
media_obj.text or ""
)
Loading
Loading