Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions openedx/core/djangoapps/content_libraries/api/block_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime

from django.utils.translation import gettext as _
from opaque_keys.edx.locator import LibraryUsageLocatorV2
Expand All @@ -15,6 +16,7 @@
__all__ = [
"LibraryXBlockMetadata",
"LibraryXBlockStaticFile",
"LibraryComponentDraftHistoryEntry",
]


Expand Down Expand Up @@ -64,6 +66,17 @@ def from_component(cls, library_key, component, associated_collections=None):
)


@dataclass(frozen=True)
class LibraryComponentDraftHistoryEntry:
"""
One entry in the draft change history of a library component.
"""
changed_by: object # AUTH_USER_MODEL instance or None
changed_at: datetime
title: str # title at time of change
action: str # "edited" | "renamed"


@dataclass(frozen=True)
class LibraryXBlockStaticFile:
"""
Expand Down
35 changes: 33 additions & 2 deletions openedx/core/djangoapps/content_libraries/api/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
InvalidNameError,
LibraryBlockAlreadyExists,
)
from .block_metadata import LibraryXBlockMetadata, LibraryXBlockStaticFile
from .block_metadata import LibraryComponentDraftHistoryEntry, LibraryXBlockMetadata, LibraryXBlockStaticFile
from .containers import (
create_container,
get_container,
Expand Down Expand Up @@ -98,6 +98,7 @@
"add_library_block_static_asset_file",
"delete_library_block_static_asset_file",
"publish_component_changes",
"get_library_component_draft_history",
]


Expand Down Expand Up @@ -191,6 +192,37 @@ def get_library_block(usage_key: LibraryUsageLocatorV2, include_collections=Fals
return xblock_metadata


def get_library_component_draft_history(usage_key: LibraryUsageLocatorV2) -> list[LibraryComponentDraftHistoryEntry]:
"""
Return the draft change history for a library component since its last publication,
ordered from most recent to oldest.

Raises ContentLibraryBlockNotFound if the component does not exist.
"""
try:
component = get_component_from_usage_key(usage_key)
except ObjectDoesNotExist as exc:
raise ContentLibraryBlockNotFound(usage_key) from exc

records = content_api.get_entity_draft_history(component.publishable_entity)

return [
LibraryComponentDraftHistoryEntry(
changed_by=record.draft_change_log.changed_by,
changed_at=record.draft_change_log.changed_at,
title=(record.new_version or record.old_version).title,
action=_resolve_draft_action(record.old_version, record.new_version),
)
for record in records
]


def _resolve_draft_action(old_version, new_version) -> str:
if old_version and new_version and old_version.title != new_version.title:
return "renamed"
return "edited"


def set_library_block_olx(usage_key: LibraryUsageLocatorV2, new_olx_str: str) -> ComponentVersion:
"""
Replace the OLX source of the given XBlock.
Expand Down Expand Up @@ -682,7 +714,6 @@ def import_staged_content_from_user_clipboard(library_key: LibraryLocatorV2, use
now,
)


def get_or_create_olx_media_type(block_type: str) -> MediaType:
"""
Get or create a MediaType for the block type.
Expand Down
19 changes: 19 additions & 0 deletions openedx/core/djangoapps/content_libraries/rest_api/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,25 @@ def delete(self, request, usage_key_str): # pylint: disable=unused-argument
return Response({})


@method_decorator(non_atomic_requests, name="dispatch")
@view_auth_classes()
class LibraryComponentDraftHistoryView(APIView):
"""
View to get the draft change history of a library component.
"""
serializer_class = serializers.LibraryComponentDraftHistoryEntrySerializer

@convert_exceptions
def get(self, request, usage_key_str):
"""
Get the draft change history for a library component since its last publication.
"""
key = LibraryUsageLocatorV2.from_string(usage_key_str)
api.require_permission_for_library_key(key.lib_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY)
history = api.get_library_component_draft_history(key)
return Response(self.serializer_class(history, many=True).data)


@method_decorator(non_atomic_requests, name="dispatch")
@view_auth_classes()
class LibraryBlockAssetListView(APIView):
Expand Down
15 changes: 15 additions & 0 deletions openedx/core/djangoapps/content_libraries/rest_api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ class LibraryXBlockMetadataSerializer(PublishableItemSerializer):
block_type = serializers.CharField(source="usage_key.block_type")


class LibraryComponentDraftHistoryEntrySerializer(serializers.Serializer):
"""
Serializer for a single entry in the draft history of a library component.
"""
changed_by = serializers.SerializerMethodField()
changed_at = serializers.DateTimeField(format=DATETIME_FORMAT, read_only=True)
title = serializers.CharField()
action = serializers.CharField()

def get_changed_by(self, obj) -> str | None:
if obj.changed_by is None:
return None
return obj.changed_by.username


class LibraryXBlockTypeSerializer(serializers.Serializer):
"""
Serializer for LibraryXBlockType
Expand Down
2 changes: 2 additions & 0 deletions openedx/core/djangoapps/content_libraries/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
path('assets/', blocks.LibraryBlockAssetListView.as_view()),
path('assets/<path:file_path>', blocks.LibraryBlockAssetView.as_view()),
path('publish/', blocks.LibraryBlockPublishView.as_view()),
# Get the draft change history for this block
path('draft_history/', blocks.LibraryComponentDraftHistoryView.as_view()),
# Future: discard changes for just this one block
])),
# Containers are Sections, Subsections, and Units
Expand Down
Loading