diff --git a/.gitignore b/.gitignore index 6123dd4cf..e7d722454 100644 --- a/.gitignore +++ b/.gitignore @@ -75,7 +75,7 @@ venv/ !.vscode/settings.json.example # Media files (for uploads) -media/ +/media/ # Media files generated during test runs -test_media/ +/test_media/ diff --git a/.importlinter b/.importlinter index 6c278d0dd..bc572f7b2 100644 --- a/.importlinter +++ b/.importlinter @@ -47,7 +47,7 @@ layers= # The "contents" app stores the simplest pieces of binary and text data, # without versioning information. These belong to a single Learning Package. - openedx_learning.apps.authoring.contents + openedx_learning.apps.authoring.media # The "collections" app stores arbitrary groupings of PublishableEntities. # Its only dependency should be the publishing app. diff --git a/olx_importer/management/commands/load_components.py b/olx_importer/management/commands/load_components.py index 55cd268c8..513591c79 100644 --- a/olx_importer/management/commands/load_components.py +++ b/olx_importer/management/commands/load_components.py @@ -29,7 +29,7 @@ # Model references to remove from openedx_learning.apps.authoring.components import api as components_api -from openedx_learning.apps.authoring.contents import api as contents_api +from openedx_learning.apps.authoring.media import api as contents_api from openedx_learning.apps.authoring.publishing import api as publishing_api SUPPORTED_TYPES = ["problem", "video", "html"] @@ -116,7 +116,7 @@ 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 = contents_api.get_or_create_file_content( + content = contents_api.get_or_create_file_media( self.learning_package.id, data=data_bytes, mime_type=mime_type, @@ -165,7 +165,7 @@ 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 = contents_api.get_or_create_text_content( + text_content, _created = contents_api.get_or_create_text_media( self.learning_package.id, text=text, mime_type=f"application/vnd.openedx.xblock.v1.{block_type_name}+xml", diff --git a/openedx_learning/api/authoring.py b/openedx_learning/api/authoring.py index 9082b33fb..b0d98cff4 100644 --- a/openedx_learning/api/authoring.py +++ b/openedx_learning/api/authoring.py @@ -12,7 +12,7 @@ from ..apps.authoring.backup_restore.api import * from ..apps.authoring.collections.api import * from ..apps.authoring.components.api import * -from ..apps.authoring.contents.api import * +from ..apps.authoring.media.api import * from ..apps.authoring.publishing.api import * from ..apps.authoring.sections.api import * from ..apps.authoring.subsections.api import * diff --git a/openedx_learning/api/authoring_models.py b/openedx_learning/api/authoring_models.py index 617d85dc4..3dd9e86ad 100644 --- a/openedx_learning/api/authoring_models.py +++ b/openedx_learning/api/authoring_models.py @@ -9,7 +9,7 @@ # pylint: disable=wildcard-import from ..apps.authoring.collections.models import * from ..apps.authoring.components.models import * -from ..apps.authoring.contents.models import * +from ..apps.authoring.media.models import * from ..apps.authoring.publishing.models import * from ..apps.authoring.sections.models import * from ..apps.authoring.subsections.models import * diff --git a/openedx_learning/apps/authoring/backup_restore/zipper.py b/openedx_learning/apps/authoring/backup_restore/zipper.py index 27ddcacc3..3fbb64ca7 100644 --- a/openedx_learning/apps/authoring/backup_restore/zipper.py +++ b/openedx_learning/apps/authoring/backup_restore/zipper.py @@ -22,8 +22,8 @@ Collection, ComponentType, ComponentVersion, - ComponentVersionContent, - Content, + ComponentVersionMedia, + Media, LearningPackage, PublishableEntity, PublishableEntityVersion, @@ -47,7 +47,7 @@ ) from openedx_learning.apps.authoring.collections import api as collections_api from openedx_learning.apps.authoring.components import api as components_api -from openedx_learning.apps.authoring.contents import api as contents_api +from openedx_learning.apps.authoring.media import api as contents_api from openedx_learning.apps.authoring.publishing import api as publishing_api from openedx_learning.apps.authoring.sections import api as sections_api from openedx_learning.apps.authoring.subsections import api as subsections_api @@ -190,13 +190,13 @@ 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"), + "draft__version__componentversion__componentversionmedia_set", + queryset=ComponentVersionMedia.objects.select_related("media"), to_attr="prefetched_contents", ), Prefetch( - "published__version__componentversion__componentversioncontent_set", - queryset=ComponentVersionContent.objects.select_related("content"), + "published__version__componentversion__componentversionmedia_set", + queryset=ComponentVersionMedia.objects.select_related("media"), to_attr="prefetched_contents", ), ) @@ -373,11 +373,11 @@ def create_zip(self, path: str) -> None: # Get content data associated with this version contents: QuerySet[ - ComponentVersionContent + ComponentVersionMedia ] = component_version.prefetched_contents # type: ignore[attr-defined] for component_version_content in contents: - content: Content = component_version_content.content + content: Media = component_version_content.media # Important: The component_version_content.key contains implicitly # the file name and the file extension @@ -984,7 +984,7 @@ def _resolve_static_files( # storing the value as a content 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_content = contents_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"), diff --git a/openedx_learning/apps/authoring/components/admin.py b/openedx_learning/apps/authoring/components/_admin.py similarity index 96% rename from openedx_learning/apps/authoring/components/admin.py rename to openedx_learning/apps/authoring/components/_admin.py index 2fa501ddf..60ca41a90 100644 --- a/openedx_learning/apps/authoring/components/admin.py +++ b/openedx_learning/apps/authoring/components/_admin.py @@ -11,7 +11,7 @@ from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin -from .models import Component, ComponentVersion, ComponentVersionContent +from .models import Component, ComponentVersion, ComponentVersionMedia class ComponentVersionInline(admin.TabularInline): @@ -54,7 +54,7 @@ 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) @@ -134,11 +134,11 @@ def format_text_for_admin_display(text: str) -> SafeText: ) -def content_preview(cvc_obj: ComponentVersionContent) -> SafeText: +def content_preview(cvc_obj: ComponentVersionMedia) -> SafeText: """ Get the HTML to display a preview of the given ComponentVersionContent """ - content_obj = cvc_obj.content + content_obj = cvc_obj.media if content_obj.media_type.type == "image": # This base64 encoding looks really goofy and is bad for performance, diff --git a/openedx_learning/apps/authoring/components/api.py b/openedx_learning/apps/authoring/components/api.py index d8867b239..a31ea2dee 100644 --- a/openedx_learning/apps/authoring/components/api.py +++ b/openedx_learning/apps/authoring/components/api.py @@ -23,9 +23,9 @@ from django.db.transaction import atomic from django.http.response import HttpResponse, HttpResponseNotFound -from ..contents import api as contents_api +from ..media import api as contents_api from ..publishing import api as publishing_api -from .models import Component, ComponentType, ComponentVersion, ComponentVersionContent +from .models import Component, ComponentType, ComponentVersion, ComponentVersionMedia # The public API that will be re-exported by openedx_learning.apps.authoring.api # is listed in the __all__ entries below. Internal helper functions that are @@ -256,7 +256,7 @@ def create_next_component_version( # RFC 2046: https://datatracker.ietf.org/doc/html/rfc2046 media_type_str = media_type_str or "application/octet-stream" media_type = contents_api.get_or_create_media_type(media_type_str) - content = contents_api.get_or_create_file_content( + content = contents_api.get_or_create_file_media( component.learning_package.id, media_type.id, data=file_content, @@ -265,8 +265,8 @@ def create_next_component_version( content_pk = content.pk else: content_pk = content_pk_or_bytes - ComponentVersionContent.objects.create( - content_id=content_pk, + ComponentVersionMedia.objects.create( + media_id=content_pk, component_version=component_version, key=key, ) @@ -276,12 +276,12 @@ def create_next_component_version( # Now copy any old associations that existed, as long as they aren't # in conflict with the new stuff or marked for deletion. - last_version_content_mapping = ComponentVersionContent.objects \ + last_version_content_mapping = ComponentVersionMedia.objects \ .filter(component_version=last_version) for cvrc in last_version_content_mapping: if cvrc.key not in content_to_replace: - ComponentVersionContent.objects.create( - content_id=cvrc.content_id, + ComponentVersionMedia.objects.create( + media_id=cvrc.media_id, component_version=component_version, key=cvrc.key, ) @@ -454,7 +454,7 @@ def look_up_component_version_content( component_key: str, version_num: int, key: Path, -) -> ComponentVersionContent: +) -> ComponentVersionMedia: """ Look up ComponentVersionContent by human readable keys. @@ -470,7 +470,7 @@ def look_up_component_version_content( & Q(component_version__publishable_entity_version__version_num=version_num) & Q(key=key) ) - return ComponentVersionContent.objects \ + return ComponentVersionMedia.objects \ .select_related( "content", "content__media_type", @@ -485,7 +485,7 @@ def create_component_version_content( content_id: int, /, key: str, -) -> ComponentVersionContent: +) -> ComponentVersionMedia: """ Add a Content to the given ComponentVersion @@ -503,9 +503,9 @@ def create_component_version_content( ) key = key.lstrip('/') - cvrc, _created = ComponentVersionContent.objects.get_or_create( + cvrc, _created = ComponentVersionMedia.objects.get_or_create( component_version_id=component_version_id, - content_id=content_id, + media_id=content_id, key=key, ) return cvrc @@ -621,8 +621,8 @@ def _error_header(error: AssetError) -> dict[str, str]: # Check: Does the ComponentVersion have the requested asset (Content)? try: - cv_content = component_version.componentversioncontent_set.get(key=asset_path) - except ComponentVersionContent.DoesNotExist: + cv_content = component_version.componentversionmedia_set.get(key=asset_path) + except ComponentVersionMedia.DoesNotExist: logger.error(f"ComponentVersion {component_version_uuid} has no asset {asset_path}") info_headers.update( _error_header(AssetError.ASSET_PATH_NOT_FOUND_FOR_COMPONENT_VERSION) @@ -634,7 +634,7 @@ def _error_header(error: AssetError) -> dict[str, str]: # anyway, but we're explicitly not doing so because streaming large text # fields from the database is less scalable, and we don't want to encourage # that usage pattern. - content = cv_content.content + content = cv_content.media if not content.has_file: logger.error( f"ComponentVersion {component_version_uuid} has asset {asset_path}, " @@ -647,7 +647,7 @@ def _error_header(error: AssetError) -> dict[str, str]: # At this point, we know that there is valid Content that we want to send. # This adds Content-level headers, like the hash/etag and content type. - info_headers.update(contents_api.get_content_info_headers(content)) + info_headers.update(contents_api.get_media_info_headers(content)) # Recompute redirect headers (reminder: this should never be cached). redirect_headers = contents_api.get_redirect_headers(content.path, public) diff --git a/openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py b/openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py index 5e6518a99..e67c54515 100644 --- a/openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py +++ b/openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py @@ -83,5 +83,5 @@ def handle(self, *args, **options): f"Created v{next_version.version_num} of " f"{next_version.component.key} ({next_version.uuid}):" ) - for cvc in next_version.componentversioncontent_set.all(): + for cvc in next_version.componentversionmedia_set.all(): self.stdout.write(f"- {cvc.key} ({cvc.uuid})") diff --git a/openedx_learning/apps/authoring/components/migrations/0005_rename_componentversioncontent_componentversionmedia.py b/openedx_learning/apps/authoring/components/migrations/0005_rename_componentversioncontent_componentversionmedia.py new file mode 100644 index 000000000..d3fdb1063 --- /dev/null +++ b/openedx_learning/apps/authoring/components/migrations/0005_rename_componentversioncontent_componentversionmedia.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.9 on 2025-12-23 19:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_components', '0004_remove_componentversioncontent_uuid'), + ('oel_contents', '0003_rename_index'), + ] + + operations = [ + migrations.RenameModel( + old_name='ComponentVersionContent', + new_name='ComponentVersionMedia', + ), + ] diff --git a/openedx_learning/apps/authoring/components/migrations/0006_rename_fields_to_use_media.py b/openedx_learning/apps/authoring/components/migrations/0006_rename_fields_to_use_media.py new file mode 100644 index 000000000..f6b36d7ec --- /dev/null +++ b/openedx_learning/apps/authoring/components/migrations/0006_rename_fields_to_use_media.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.9 on 2025-12-23 19:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_components', '0005_rename_componentversioncontent_componentversionmedia'), + ('oel_contents', '0003_rename_index'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='componentversionmedia', + name='oel_cvcontent_uniq_cv_key', + ), + migrations.RemoveIndex( + model_name='componentversionmedia', + name='oel_cvcontent_c_cv', + ), + migrations.RemoveIndex( + model_name='componentversionmedia', + name='oel_cvcontent_cv_d', + ), + migrations.RenameField( + model_name='componentversion', + old_name='contents', + new_name='media', + ), + migrations.RenameField( + model_name='componentversionmedia', + old_name='content', + new_name='media', + ), + migrations.AddIndex( + model_name='componentversionmedia', + index=models.Index(fields=['media', 'component_version'], name='oel_cvmediat_c_cv'), + ), + migrations.AddIndex( + model_name='componentversionmedia', + index=models.Index(fields=['component_version', 'media'], name='oel_cvmedia_cv_d'), + ), + migrations.AddConstraint( + model_name='componentversionmedia', + constraint=models.UniqueConstraint(fields=('component_version', 'key'), name='oel_cvmedia_uniq_cv_key'), + ), + ] diff --git a/openedx_learning/apps/authoring/components/models.py b/openedx_learning/apps/authoring/components/models.py index b53077637..3d34cfebe 100644 --- a/openedx_learning/apps/authoring/components/models.py +++ b/openedx_learning/apps/authoring/components/models.py @@ -23,14 +23,14 @@ from ....lib.fields import case_sensitive_char_field, key_field from ....lib.managers import WithRelationsManager -from ..contents.models import Content +from ..media.models import Media from ..publishing.models import LearningPackage, PublishableEntityMixin, PublishableEntityVersionMixin __all__ = [ "ComponentType", "Component", "ComponentVersion", - "ComponentVersionContent", + "ComponentVersionMedia", ] @@ -198,7 +198,7 @@ class ComponentVersion(PublishableEntityVersionMixin): A particular version of a Component. This holds the content using a M:M relationship with Content via - ComponentVersionContent. + ComponentVersionMedia. """ # This is technically redundant, since we can get this through @@ -208,20 +208,26 @@ class ComponentVersion(PublishableEntityVersionMixin): Component, on_delete=models.CASCADE, related_name="versions" ) - # The contents hold the actual interesting data associated with this + # The media relation holds the actual interesting data associated with this # ComponentVersion. - contents: models.ManyToManyField[Content, ComponentVersionContent] = models.ManyToManyField( - Content, - through="ComponentVersionContent", + media: models.ManyToManyField[Media, ComponentVersionMedia] = models.ManyToManyField( + Media, + through="ComponentVersionMedia", related_name="component_versions", ) + @property + def contents(self): + """Backwards compatibility shim.""" + return self.media + + class Meta: verbose_name = "Component Version" verbose_name_plural = "Component Versions" -class ComponentVersionContent(models.Model): +class ComponentVersionMedia(models.Model): """ Determines the Content for a given ComponentVersion. @@ -238,7 +244,7 @@ class ComponentVersionContent(models.Model): """ component_version = models.ForeignKey(ComponentVersion, on_delete=models.CASCADE) - content = models.ForeignKey(Content, on_delete=models.RESTRICT) + media = models.ForeignKey(Media, on_delete=models.RESTRICT) # "key" is a reserved word for MySQL, so we're temporarily using the column # name of "_key" to avoid breaking downstream tooling. A possible @@ -254,16 +260,16 @@ class Meta: # with two different identifiers, that is permitted. models.UniqueConstraint( fields=["component_version", "key"], - name="oel_cvcontent_uniq_cv_key", + name="oel_cvmedia_uniq_cv_key", ), ] indexes = [ models.Index( - fields=["content", "component_version"], - name="oel_cvcontent_c_cv", + fields=["media", "component_version"], + name="oel_cvmediat_c_cv", ), models.Index( - fields=["component_version", "content"], - name="oel_cvcontent_cv_d", + fields=["component_version", "media"], + name="oel_cvmedia_cv_d", ), ] diff --git a/openedx_learning/apps/authoring/contents/__init__.py b/openedx_learning/apps/authoring/media/__init__.py similarity index 100% rename from openedx_learning/apps/authoring/contents/__init__.py rename to openedx_learning/apps/authoring/media/__init__.py diff --git a/openedx_learning/apps/authoring/contents/admin.py b/openedx_learning/apps/authoring/media/admin.py similarity index 84% rename from openedx_learning/apps/authoring/contents/admin.py rename to openedx_learning/apps/authoring/media/admin.py index 029ebfd86..57cf254cb 100644 --- a/openedx_learning/apps/authoring/contents/admin.py +++ b/openedx_learning/apps/authoring/media/admin.py @@ -8,11 +8,11 @@ from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin -from .models import Content +from .models import Media -@admin.register(Content) -class ContentAdmin(ReadOnlyModelAdmin): +@admin.register(Media) +class MediaAdmin(ReadOnlyModelAdmin): """ Django admin for Content model """ @@ -40,13 +40,13 @@ class ContentAdmin(ReadOnlyModelAdmin): search_fields = ("hash_digest",) @admin.display(description="OS Path") - def os_path(self, content: Content): + def os_path(self, content: Media): return content.os_path() or "" - def path(self, content: Content): + def path(self, content: Media): return content.path if content.has_file else "" - def text_preview(self, content: Content): + def text_preview(self, content: Media): if not content.text: return "" return format_html( @@ -54,7 +54,7 @@ def text_preview(self, content: Content): content.text, ) - def image_preview(self, content: Content): + def image_preview(self, content: Media): """ Return HTML for an image, if that is the underlying Content. diff --git a/openedx_learning/apps/authoring/contents/api.py b/openedx_learning/apps/authoring/media/api.py similarity index 79% rename from openedx_learning/apps/authoring/contents/api.py rename to openedx_learning/apps/authoring/media/api.py index 319935882..1baa1d7a1 100644 --- a/openedx_learning/apps/authoring/contents/api.py +++ b/openedx_learning/apps/authoring/media/api.py @@ -1,5 +1,5 @@ """ -Low Level Contents API (warning: UNSTABLE, in progress API) +Low Level Media API (warning: UNSTABLE, in progress API) Please look at the models.py file for more information about the kinds of data are stored in this app. @@ -13,7 +13,7 @@ from django.db.transaction import atomic from ....lib.fields import create_hash_digest -from .models import Content, MediaType +from .models import Media, MediaType # The public API that will be re-exported by openedx_learning.apps.authoring.api # is listed in the __all__ entries below. Internal helper functions that are @@ -22,10 +22,10 @@ # to be callable only by other apps in the authoring package. __all__ = [ "get_or_create_media_type", - "get_content", - "get_content_info_headers", - "get_or_create_text_content", - "get_or_create_file_content", + "get_media", + "get_media_info_headers", + "get_or_create_text_media", + "get_or_create_file_media", ] @@ -66,29 +66,29 @@ def get_or_create_media_type(mime_type: str) -> MediaType: return media_type -def get_content(content_id: int, /) -> Content: +def get_media(media_id: int, /) -> Media: """ - Get a single Content object by its ID. + Get a single Media object by its ID. - Content is always attached to something when it's created, like to a - ComponentVersion. That means the "right" way to access a Content is almost + Media is always attached to something when it's created, like to a + ComponentVersion. That means the "right" way to access a Media is almost always going to be via those relations and not via this function. But I include this function anyway because it's tiny to write and it's better than someone using a get_or_create_* function when they really just want to get. """ - return Content.objects.get(id=content_id) + return Media.objects.get(id=media_id) -def get_or_create_text_content( +def get_or_create_text_media( learning_package_id: int, media_type_id: int, /, text: str, created: datetime, create_file: bool = False, -) -> Content: +) -> Media: """ - Get or create a Content entry with text data stored in the database. + Get or create a Media entry with text data stored in the database. Use this when you want to create relatively small chunks of text that need to be accessed quickly, especially if you're pulling back multiple rows at @@ -101,7 +101,7 @@ def get_or_create_text_content( that file be downloadable by browsers in the LMS at some point. If you want to create a large text file, or want to create a text file that - doesn't need to be stored in the database, call ``create_file_content`` + doesn't need to be stored in the database, call ``create_file_media`` instead of this function. """ text_as_bytes = text.encode('utf-8') @@ -109,13 +109,13 @@ def get_or_create_text_content( with atomic(): try: - content = Content.objects.get( + media = Media.objects.get( learning_package_id=learning_package_id, media_type_id=media_type_id, hash_digest=hash_digest, ) - except Content.DoesNotExist: - content = Content( + except Media.DoesNotExist: + media = Media( learning_package_id=learning_package_id, media_type_id=media_type_id, hash_digest=hash_digest, @@ -124,41 +124,41 @@ def get_or_create_text_content( text=text, has_file=create_file, ) - content.full_clean() - content.save() + media.full_clean() + media.save() if create_file: - content.write_file(ContentFile(text_as_bytes)) + media.write_file(ContentFile(text_as_bytes)) - return content + return media -def get_or_create_file_content( +def get_or_create_file_media( learning_package_id: int, media_type_id: int, /, data: bytes, created: datetime, -) -> Content: +) -> Media: """ - Get or create a Content with data stored in a file storage backend. + Get or create a Media with data stored in a file storage backend. Use this function to store non-text data, large data, or data where low latency access is not necessary. Also use this function (or - ``get_or_create_text_content`` with ``create_file=True``) to store any - Content that you want to be downloadable by browsers in the LMS, since the - static asset serving system will only work with file-backed Content. + ``get_or_create_text_media`` with ``create_file=True``) to store any + Media that you want to be downloadable by browsers in the LMS, since the + static asset serving system will only work with file-backed Media. """ hash_digest = create_hash_digest(data) with atomic(): try: - content = Content.objects.get( + media = Media.objects.get( learning_package_id=learning_package_id, media_type_id=media_type_id, hash_digest=hash_digest, ) - except Content.DoesNotExist: - content = Content( + except Media.DoesNotExist: + media = Media( learning_package_id=learning_package_id, media_type_id=media_type_id, hash_digest=hash_digest, @@ -167,24 +167,24 @@ def get_or_create_file_content( text=None, has_file=True, ) - content.full_clean() - content.save() + media.full_clean() + media.save() - content.write_file(ContentFile(data)) + media.write_file(ContentFile(data)) - return content + return media -def get_content_info_headers(content: Content) -> dict[str, str]: +def get_media_info_headers(media: Media) -> dict[str, str]: """ - Return HTTP headers that are specific to this Content. + Return HTTP headers that are specific to this Media. This currently only consists of the Content-Type and ETag. These values are safe to cache. """ return { - "Content-Type": str(content.media_type), - "Etag": content.hash_digest, + "Content-Type": str(media.media_type), + "Etag": media.hash_digest, } @@ -196,7 +196,7 @@ def get_redirect_headers( """ Return a dict of headers for file redirect and caching. - This is a separate function from get_content_info_headers because the URLs + This is a separate function from get_media_info_headers because the URLs returned in these headers produced by this function should never be put into the backend Django cache (redis/memcached). The `stored_file_path` location *is* cacheable though–that's the actual storage location for the resource, @@ -228,7 +228,7 @@ def get_redirect_headers( cache_directive = "private" # This only stays on the user's browser, so cache for a whole day. This - # is okay to do because Content data is typically immutable–i.e. if an + # is okay to do because Media data is typically immutable–i.e. if an # asset actually changes, the user should be directed to a different URL # for it. max_age = max_age or (60 * 60 * 24) diff --git a/openedx_learning/apps/authoring/contents/apps.py b/openedx_learning/apps/authoring/media/apps.py similarity index 71% rename from openedx_learning/apps/authoring/contents/apps.py rename to openedx_learning/apps/authoring/media/apps.py index 3c23bd4f5..e0168b663 100644 --- a/openedx_learning/apps/authoring/contents/apps.py +++ b/openedx_learning/apps/authoring/media/apps.py @@ -9,7 +9,7 @@ class ContentsConfig(AppConfig): Configuration for the Contents Django application. """ - name = "openedx_learning.apps.authoring.contents" - verbose_name = "Learning Core > Authoring > Contents" + name = "openedx_learning.apps.authoring.media" + verbose_name = "Learning Core > Authoring > Media" default_auto_field = "django.db.models.BigAutoField" label = "oel_contents" diff --git a/openedx_learning/apps/authoring/contents/migrations/0001_initial.py b/openedx_learning/apps/authoring/media/migrations/0001_initial.py similarity index 100% rename from openedx_learning/apps/authoring/contents/migrations/0001_initial.py rename to openedx_learning/apps/authoring/media/migrations/0001_initial.py diff --git a/openedx_learning/apps/authoring/media/migrations/0002_rename_content_media.py b/openedx_learning/apps/authoring/media/migrations/0002_rename_content_media.py new file mode 100644 index 000000000..809afb276 --- /dev/null +++ b/openedx_learning/apps/authoring/media/migrations/0002_rename_content_media.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.9 on 2025-12-22 23:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_components', '0004_remove_componentversioncontent_uuid'), + ('oel_contents', '0001_initial'), + ('oel_publishing', '0010_backfill_dependencies'), + ] + + operations = [ + migrations.RenameModel( + old_name='Content', + new_name='Media', + ), + migrations.AlterModelOptions( + name='media', + options={'verbose_name': 'Media', 'verbose_name_plural': 'Media'}, + ), + ] diff --git a/openedx_learning/apps/authoring/media/migrations/0003_rename_index.py b/openedx_learning/apps/authoring/media/migrations/0003_rename_index.py new file mode 100644 index 000000000..9612843db --- /dev/null +++ b/openedx_learning/apps/authoring/media/migrations/0003_rename_index.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.9 on 2025-12-23 19:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_contents', '0002_rename_content_media'), + ('oel_publishing', '0010_backfill_dependencies'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='media', + name='oel_content_uniq_lc_media_type_hash_digest', + ), + migrations.RenameIndex( + model_name='media', + new_name='oel_media_idx_lp_rsize', + old_name='oel_content_idx_lp_rsize', + ), + migrations.AddConstraint( + model_name='media', + constraint=models.UniqueConstraint(fields=('learning_package', 'media_type', 'hash_digest'), name='oel_media_uniq_lc_media_type_hash_digest'), + ), + ] diff --git a/openedx_learning/apps/authoring/contents/migrations/__init__.py b/openedx_learning/apps/authoring/media/migrations/__init__.py similarity index 100% rename from openedx_learning/apps/authoring/contents/migrations/__init__.py rename to openedx_learning/apps/authoring/media/migrations/__init__.py diff --git a/openedx_learning/apps/authoring/contents/models.py b/openedx_learning/apps/authoring/media/models.py similarity index 98% rename from openedx_learning/apps/authoring/contents/models.py rename to openedx_learning/apps/authoring/media/models.py index c087c122f..4af53925b 100644 --- a/openedx_learning/apps/authoring/contents/models.py +++ b/openedx_learning/apps/authoring/media/models.py @@ -24,7 +24,7 @@ __all__ = [ "MediaType", - "Content", + "Media", ] @@ -128,7 +128,7 @@ def __str__(self) -> str: return base -class Content(models.Model): +class Media(models.Model): """ This is the most primitive piece of content data. @@ -233,7 +233,7 @@ class Content(models.Model): # could be as much as 200K of data if we had nothing but emojis. MAX_TEXT_LENGTH = 50_000 - objects: models.Manager[Content] = WithRelationsManager('media_type') + objects: models.Manager[Media] = WithRelationsManager('media_type') learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE) @@ -395,7 +395,7 @@ class Meta: "media_type", "hash_digest", ], - name="oel_content_uniq_lc_media_type_hash_digest", + name="oel_media_uniq_lc_media_type_hash_digest", ), ] indexes = [ @@ -403,8 +403,8 @@ class Meta: # * Find the largest Content entries. models.Index( fields=["learning_package", "-size"], - name="oel_content_idx_lp_rsize", + name="oel_media_idx_lp_rsize", ), ] - verbose_name = "Content" - verbose_name_plural = "Contents" + verbose_name = "Media" + verbose_name_plural = "Media" diff --git a/projects/dev.py b/projects/dev.py index 1b3b42f47..f233b9b84 100644 --- a/projects/dev.py +++ b/projects/dev.py @@ -33,7 +33,7 @@ # Learning Core Apps "openedx_learning.apps.authoring.collections.apps.CollectionsConfig", "openedx_learning.apps.authoring.components.apps.ComponentsConfig", - "openedx_learning.apps.authoring.contents.apps.ContentsConfig", + "openedx_learning.apps.authoring.media.apps.ContentsConfig", "openedx_learning.apps.authoring.publishing.apps.PublishingConfig", "openedx_learning.apps.authoring.sections.apps.SectionsConfig", "openedx_learning.apps.authoring.subsections.apps.SubsectionsConfig", diff --git a/test_settings.py b/test_settings.py index c4b22926d..24129e2de 100644 --- a/test_settings.py +++ b/test_settings.py @@ -29,7 +29,7 @@ def root(*args): # If you provision the 'oel'@'%' with broad permissions on your MySQL instance, # running the tests will auto-generate a database for running tests. This is # slower than the default sqlite3 setup above, but it's sometimes helpful for -# finding things that only break in CI. +# finding things that only break in CI. # # DATABASES = { # "default": { @@ -55,15 +55,15 @@ def root(*args): # django-rules based authorization 'rules.apps.AutodiscoverRulesConfig', # Our own apps - "openedx_learning.apps.authoring.collections.apps.CollectionsConfig", - "openedx_learning.apps.authoring.components.apps.ComponentsConfig", - "openedx_learning.apps.authoring.contents.apps.ContentsConfig", - "openedx_learning.apps.authoring.publishing.apps.PublishingConfig", - "openedx_tagging.core.tagging.apps.TaggingConfig", - "openedx_learning.apps.authoring.sections.apps.SectionsConfig", - "openedx_learning.apps.authoring.subsections.apps.SubsectionsConfig", - "openedx_learning.apps.authoring.units.apps.UnitsConfig", - "openedx_learning.apps.authoring.backup_restore.apps.BackupRestoreConfig", + "openedx_learning.apps.authoring.collections", + "openedx_learning.apps.authoring.components", + "openedx_learning.apps.authoring.media", + "openedx_learning.apps.authoring.publishing", + "openedx_tagging.core.tagging", + "openedx_learning.apps.authoring.sections", + "openedx_learning.apps.authoring.subsections", + "openedx_learning.apps.authoring.units", + "openedx_learning.apps.authoring.backup_restore", ] AUTHENTICATION_BACKENDS = [ diff --git a/tests/openedx_learning/apps/authoring/backup_restore/test_backup.py b/tests/openedx_learning/apps/authoring/backup_restore/test_backup.py index 312f289f1..75116b6a5 100644 --- a/tests/openedx_learning/apps/authoring/backup_restore/test_backup.py +++ b/tests/openedx_learning/apps/authoring/backup_restore/test_backup.py @@ -11,7 +11,7 @@ from django.db.models import QuerySet from openedx_learning.api import authoring as api -from openedx_learning.api.authoring_models import Collection, Component, Content, LearningPackage, PublishableEntity +from openedx_learning.api.authoring_models import Collection, Component, Media, LearningPackage, PublishableEntity from openedx_learning.apps.authoring.backup_restore.zipper import LearningPackageZipper from openedx_learning.lib.test_utils import TestCase @@ -32,7 +32,7 @@ class LpDumpCommandTestCase(TestCase): published_component: Component published_component2: Component draft_component: Component - html_asset_content: Content + html_asset_content: Media collection: Collection @classmethod @@ -100,7 +100,7 @@ def setUpTestData(cls): created=cls.now, ) - new_txt_content = api.get_or_create_text_content( + new_txt_content = api.get_or_create_text_media( cls.learning_package.pk, text_media_type.id, text="This is some data", @@ -129,7 +129,7 @@ def setUpTestData(cls): created=cls.now, ) - cls.html_asset_content = api.get_or_create_file_content( + cls.html_asset_content = api.get_or_create_file_media( cls.learning_package.id, html_media_type.id, data=b"hello world!", diff --git a/tests/openedx_learning/apps/authoring/components/test_api.py b/tests/openedx_learning/apps/authoring/components/test_api.py index 8a4dd44c5..fd5780ded 100644 --- a/tests/openedx_learning/apps/authoring/components/test_api.py +++ b/tests/openedx_learning/apps/authoring/components/test_api.py @@ -11,8 +11,8 @@ from openedx_learning.apps.authoring.collections.models import Collection from openedx_learning.apps.authoring.components import api as components_api from openedx_learning.apps.authoring.components.models import Component, ComponentType -from openedx_learning.apps.authoring.contents import api as contents_api -from openedx_learning.apps.authoring.contents.models import MediaType +from openedx_learning.apps.authoring.media import api as contents_api +from openedx_learning.apps.authoring.media.models import MediaType from openedx_learning.apps.authoring.publishing import api as publishing_api from openedx_learning.apps.authoring.publishing.models import LearningPackage from openedx_learning.lib.test_utils import TestCase @@ -400,7 +400,7 @@ def test_add(self): created=self.now, created_by=None, ) - new_content = contents_api.get_or_create_text_content( + new_content = contents_api.get_or_create_text_media( self.learning_package.pk, self.text_media_type.id, text="This is some data", @@ -417,7 +417,7 @@ def test_add(self): .get(publishable_entity_version__version_num=1) assert ( new_content == - new_version.contents.get(componentversioncontent__key="my/path/to/hello.txt") + new_version.contents.get(componentversionmedia__key="my/path/to/hello.txt") ) # Write the same content again, but to an absolute path (should auto- @@ -432,7 +432,7 @@ def test_add(self): .get(publishable_entity_version__version_num=1) assert ( new_content == - new_version.contents.get(componentversioncontent__key="nested/path/hello.txt") + new_version.contents.get(componentversionmedia__key="nested/path/hello.txt") ) def test_bytes_content(self): @@ -448,8 +448,8 @@ def test_bytes_content(self): created=self.now, ) - content_txt = version_1.contents.get(componentversioncontent__key="raw.txt") - content_raw_txt = version_1.contents.get(componentversioncontent__key="no_ext") + content_txt = version_1.media.get(componentversionmedia__key="raw.txt") + content_raw_txt = version_1.media.get(componentversionmedia__key="no_ext") assert content_txt.size == len(bytes_content) assert str(content_txt.media_type) == 'text/plain' @@ -460,19 +460,19 @@ def test_bytes_content(self): assert content_raw_txt.read_file().read() == bytes_content def test_multiple_versions(self): - hello_content = contents_api.get_or_create_text_content( + hello_content = contents_api.get_or_create_text_media( self.learning_package.id, self.text_media_type.id, text="Hello World!", created=self.now, ) - goodbye_content = contents_api.get_or_create_text_content( + goodbye_content = contents_api.get_or_create_text_media( self.learning_package.id, self.text_media_type.id, text="Goodbye World!", created=self.now, ) - blank_content = contents_api.get_or_create_text_content( + blank_content = contents_api.get_or_create_text_media( self.learning_package.id, self.text_media_type.id, text="", @@ -491,17 +491,17 @@ def test_multiple_versions(self): ) assert version_1.version_num == 1 assert version_1.title == "Problem Version 1" - version_1_contents = list(version_1.contents.all()) + version_1_contents = list(version_1.media.all()) assert len(version_1_contents) == 2 assert ( hello_content == - version_1.contents - .get(componentversioncontent__key="hello.txt") + version_1.media + .get(componentversionmedia__key="hello.txt") ) assert ( goodbye_content == - version_1.contents - .get(componentversioncontent__key="goodbye.txt") + version_1.media + .get(componentversionmedia__key="goodbye.txt") ) # This should keep the old value for goodbye.txt, add blank.txt, and set @@ -516,21 +516,21 @@ def test_multiple_versions(self): created=self.now, ) assert version_2.version_num == 2 - assert version_2.contents.count() == 3 + assert version_2.media.count() == 3 assert ( blank_content == - version_2.contents - .get(componentversioncontent__key="hello.txt") + version_2.media + .get(componentversionmedia__key="hello.txt") ) assert ( goodbye_content == - version_2.contents - .get(componentversioncontent__key="goodbye.txt") + version_2.media + .get(componentversionmedia__key="goodbye.txt") ) assert ( blank_content == - version_2.contents - .get(componentversioncontent__key="blank.txt") + version_2.media + .get(componentversionmedia__key="blank.txt") ) # Now we're going to set "hello.txt" back to hello_content, but remove @@ -547,11 +547,11 @@ def test_multiple_versions(self): created=self.now, ) assert version_3.version_num == 3 - assert version_3.contents.count() == 1 + assert version_3.media.count() == 1 assert ( hello_content == - version_3.contents - .get(componentversioncontent__key="hello.txt") + version_3.media + .get(componentversionmedia__key="hello.txt") ) def test_create_next_version_forcing_num_version(self): @@ -573,7 +573,7 @@ def test_create_multiple_next_versions_and_diff_content(self): python_source_media_type = contents_api.get_or_create_media_type( "text/x-python", ) - python_source_asset = contents_api.get_or_create_file_content( + python_source_asset = contents_api.get_or_create_file_media( self.learning_package.id, python_source_media_type.id, data=b"print('hello world!')", @@ -609,21 +609,21 @@ def test_create_multiple_next_versions_and_diff_content(self): ignore_previous_content=True, ) assert version_2_draft.version_num == 2 - assert version_2_draft.contents.count() == 2 + assert version_2_draft.media.count() == 2 assert ( python_source_asset == - version_2_draft.contents.get( - componentversioncontent__key="static/profile.webp") + version_2_draft.media.get( + componentversionmedia__key="static/profile.webp") ) assert ( python_source_asset == - version_2_draft.contents.get( - componentversioncontent__key="static/new_file.webp") + version_2_draft.media.get( + componentversionmedia__key="static/new_file.webp") ) with self.assertRaises(ObjectDoesNotExist): # This file was in the published version, but not in the draft version # since we ignored previous content. - version_2_draft.contents.get(componentversioncontent__key="static/background.webp") + version_2_draft.media.get(componentversionmedia__key="static/background.webp") class SetCollectionsTestCase(ComponentTestCase): diff --git a/tests/openedx_learning/apps/authoring/components/test_assets.py b/tests/openedx_learning/apps/authoring/components/test_assets.py index f9cbf0643..611653048 100644 --- a/tests/openedx_learning/apps/authoring/components/test_assets.py +++ b/tests/openedx_learning/apps/authoring/components/test_assets.py @@ -7,7 +7,7 @@ from openedx_learning.apps.authoring.components import api as components_api from openedx_learning.apps.authoring.components.api import AssetError -from openedx_learning.apps.authoring.contents import api as contents_api +from openedx_learning.apps.authoring.media import api as contents_api from openedx_learning.apps.authoring.publishing import api as publishing_api from openedx_learning.apps.authoring.publishing.models import LearningPackage from openedx_learning.lib.test_utils import TestCase @@ -25,9 +25,9 @@ class AssetTestCase(TestCase): component: components_api.Component component_version: components_api.ComponentVersion - problem_content: contents_api.Content - python_source_asset: contents_api.Content - html_asset_content: contents_api.Content + problem_content: contents_api.Media + python_source_asset: contents_api.Media + html_asset_content: contents_api.Media learning_package: LearningPackage now: datetime @@ -65,7 +65,7 @@ def setUpTestData(cls) -> None: ) # ProblemBlock content that is stored as text Content, not a file. - cls.problem_content = contents_api.get_or_create_text_content( + cls.problem_content = contents_api.get_or_create_text_media( cls.learning_package.id, cls.problem_block_media_type.id, text="(pretend problem OLX is here)", @@ -79,7 +79,7 @@ def setUpTestData(cls) -> None: # Python source file, stored as a file. This is hypothetical, as we # don't actually support bundling grader files like this today. - cls.python_source_asset = contents_api.get_or_create_file_content( + cls.python_source_asset = contents_api.get_or_create_file_media( cls.learning_package.id, cls.python_source_media_type.id, data=b"print('hello world!')", @@ -92,7 +92,7 @@ def setUpTestData(cls) -> None: ) # An HTML file that is student downloadable - cls.html_asset_content = contents_api.get_or_create_file_content( + cls.html_asset_content = contents_api.get_or_create_file_media( cls.learning_package.id, cls.html_media_type.id, data=b"hello world!", diff --git a/tests/openedx_learning/apps/authoring/contents/__init__.py b/tests/openedx_learning/apps/authoring/media/__init__.py similarity index 100% rename from tests/openedx_learning/apps/authoring/contents/__init__.py rename to tests/openedx_learning/apps/authoring/media/__init__.py diff --git a/tests/openedx_learning/apps/authoring/contents/test_file_storage.py b/tests/openedx_learning/apps/authoring/media/test_file_storage.py similarity index 93% rename from tests/openedx_learning/apps/authoring/contents/test_file_storage.py rename to tests/openedx_learning/apps/authoring/media/test_file_storage.py index a94b31571..ec40a9bd1 100644 --- a/tests/openedx_learning/apps/authoring/contents/test_file_storage.py +++ b/tests/openedx_learning/apps/authoring/media/test_file_storage.py @@ -7,8 +7,8 @@ from django.core.exceptions import ImproperlyConfigured from django.test import override_settings -from openedx_learning.apps.authoring.contents import api as contents_api -from openedx_learning.apps.authoring.contents.models import get_storage +from openedx_learning.apps.authoring.media import api as contents_api +from openedx_learning.apps.authoring.media.models import get_storage from openedx_learning.apps.authoring.publishing import api as publishing_api from openedx_learning.lib.test_utils import TestCase @@ -34,7 +34,7 @@ def setUp(self) -> None: title="Content File Storage Test Case Learning Package", ) self.html_media_type = contents_api.get_or_create_media_type("text/html") - self.html_content = contents_api.get_or_create_file_content( + self.html_content = contents_api.get_or_create_file_media( learning_package.id, self.html_media_type.id, data=b"hello world!", diff --git a/tests/openedx_learning/apps/authoring/contents/test_media_types.py b/tests/openedx_learning/apps/authoring/media/test_media_types.py similarity index 92% rename from tests/openedx_learning/apps/authoring/contents/test_media_types.py rename to tests/openedx_learning/apps/authoring/media/test_media_types.py index 6f9b16b30..f24be1880 100644 --- a/tests/openedx_learning/apps/authoring/contents/test_media_types.py +++ b/tests/openedx_learning/apps/authoring/media/test_media_types.py @@ -1,7 +1,7 @@ """ A few tests to make sure our MediaType lookups are working as expected. """ -from openedx_learning.apps.authoring.contents import api as contents_api +from openedx_learning.apps.authoring.media import api as contents_api from openedx_learning.lib.test_utils import TestCase