diff --git a/openedx/core/djangoapps/content_libraries/api/block_metadata.py b/openedx/core/djangoapps/content_libraries/api/block_metadata.py index 6faa031cf78c..cb76aee22ed1 100644 --- a/openedx/core/djangoapps/content_libraries/api/block_metadata.py +++ b/openedx/core/djangoapps/content_libraries/api/block_metadata.py @@ -95,6 +95,8 @@ class LibraryHistoryEntry: title: str # title at time of change item_type: str action: str # "created" | "edited" | "renamed" | "deleted" + old_version: int + new_version: int | None @dataclass(frozen=True) diff --git a/openedx/core/djangoapps/content_libraries/api/blocks.py b/openedx/core/djangoapps/content_libraries/api/blocks.py index 892bc3a62b28..e23322d3b29b 100644 --- a/openedx/core/djangoapps/content_libraries/api/blocks.py +++ b/openedx/core/djangoapps/content_libraries/api/blocks.py @@ -220,12 +220,18 @@ def _contributor(user): entries = [] for record in draft_change_records: version = record.new_version if record.new_version is not None else record.old_version + # old_version is None only for the very first publish (entity had no prior published version) + old_version_num = record.old_version.version_num if record.old_version else 0 + # new_version is None for soft-delete publishes (component deleted without a new draft version) + new_version_num = record.new_version.version_num if record.new_version else None entries.append(LibraryHistoryEntry( contributor=_contributor(record.draft_change_log.changed_by), changed_at=record.draft_change_log.changed_at, title=version.title if version is not None else "", item_type=record.entity.component.component_type.name, action=resolve_change_action(record.old_version, record.new_version), + old_version=old_version_num, + new_version=new_version_num, )) return entries @@ -351,12 +357,18 @@ def _contributor(user): # Deleted components can't reach this endpoint, so new_version is always set. # (Unlike containers — see get_library_container_publish_history_entries.) assert record.new_version is not None # for satisfy the type check + # old_version is None only for the very first publish (entity had no prior published version) + old_version_num = record.old_version.version_num if record.old_version else 0 + # new_version is None for soft-delete publishes (component deleted without a new draft version) + new_version_num = record.new_version.version_num if record.new_version else None entries.append(LibraryHistoryEntry( contributor=_contributor(record.draft_change_log.changed_by), changed_at=record.draft_change_log.changed_at, title=record.new_version.title, item_type=record.entity.component.component_type.name, action=resolve_change_action(record.old_version, record.new_version), + old_version=old_version_num, + new_version=new_version_num, )) return entries @@ -396,6 +408,8 @@ def get_library_component_creation_entry( title=first_version.title, item_type=component.component_type.name, action="created", + old_version=0, + new_version=first_version.version_num, ) diff --git a/openedx/core/djangoapps/content_libraries/api/containers.py b/openedx/core/djangoapps/content_libraries/api/containers.py index 0fbe66518d44..9bb54c5daba3 100644 --- a/openedx/core/djangoapps/content_libraries/api/containers.py +++ b/openedx/core/djangoapps/content_libraries/api/containers.py @@ -346,6 +346,10 @@ def _contributor(user): # Use the new version when available; fall back to the old version # (e.g. for delete records where new_version is None). version = record.new_version if record.new_version is not None else record.old_version + # old_version is None only for the very first publish (entity had no prior published version) + old_version_num = record.old_version.version_num if record.old_version else 0 + # new_version is None for soft-delete publishes (container deleted without a new draft version) + new_version_num = record.new_version.version_num if record.new_version else None item_type = get_entity_item_type(record.entity) results.append(LibraryHistoryEntry( contributor=_contributor(record.draft_change_log.changed_by), @@ -353,6 +357,8 @@ def _contributor(user): title=version.title if version is not None else "", item_type=item_type, action=resolve_change_action(record.old_version, record.new_version), + old_version=old_version_num, + new_version=new_version_num, )) # Return all entries sorted newest-first across the container and its children. @@ -576,6 +582,10 @@ def _contributor(user): for record in records: version = record.new_version if record.new_version is not None else record.old_version + # old_version is None only for the very first publish (entity had no prior published version) + old_version_num = record.old_version.version_num if record.old_version else 0 + # new_version is None for soft-delete publishes (component deleted without a new draft version) + new_version_num = record.new_version.version_num if record.new_version else None item_type = get_entity_item_type(record.entity) entries.append(LibraryHistoryEntry( contributor=_contributor(record.draft_change_log.changed_by), @@ -583,6 +593,8 @@ def _contributor(user): title=version.title if version is not None else "", item_type=item_type, action=resolve_change_action(record.old_version, record.new_version), + old_version=old_version_num, + new_version=new_version_num, )) # Return entries sorted newest-first; use title as tiebreaker for determinism. @@ -619,4 +631,6 @@ def get_library_container_creation_entry( title=first_version.title, item_type=container.container_type.type_code, action="created", + old_version=0, + new_version=first_version.version_num, ) diff --git a/openedx/core/djangoapps/content_libraries/rest_api/serializers.py b/openedx/core/djangoapps/content_libraries/rest_api/serializers.py index 8a18be6c162a..cdfa2771d2f2 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/serializers.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/serializers.py @@ -194,6 +194,8 @@ class LibraryHistoryEntrySerializer(serializers.Serializer): title = serializers.CharField(read_only=True) item_type = serializers.CharField(read_only=True) action = serializers.CharField(read_only=True) + old_version = serializers.IntegerField(read_only=True) + new_version = serializers.IntegerField(read_only=True, allow_null=True) class UsageKeyV2Serializer(serializers.BaseSerializer): diff --git a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py index 6d947aa786d4..903e7e43a23e 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py @@ -977,6 +977,8 @@ def test_draft_history_action_created(self): history = self._get_block_draft_history(block_key) assert len(history) >= 1 assert history[-1]["action"] == "created" + assert history[-1]["old_version"] == 0 + assert history[-1]["new_version"] is not None def test_draft_history_action_deleted(self): """ @@ -1153,6 +1155,25 @@ def test_publish_history_entries(self): assert "changed_at" in entry assert "title" in entry assert "action" in entry + assert "old_version" in entry + assert "new_version" in entry + + def test_draft_history_deleted_has_null_new_version(self): + """ + Deleted draft history entry exposes new_version as null. + """ + lib = self._create_library(slug="draft-hist-delete-null", title="Draft History Delete Null") + block = self._add_block_to_library(lib["id"], "problem", "prob1") + block_key = block["id"] + + self._publish_library_block(block_key) + self._delete_library_block(block_key) + + history = self._get_block_draft_history(block_key) + assert len(history) >= 1 + assert history[0]["action"] == "deleted" + assert history[0]["old_version"] > 0 + assert history[0]["new_version"] is None def test_publish_history_entries_unknown_uuid(self): """ @@ -1436,6 +1457,8 @@ def test_creation_entry_returns_first_version(self): assert entry is not None assert entry["action"] == "created" assert entry["item_type"] == "problem" + assert entry["old_version"] == 0 + assert entry["new_version"] == 1 assert "changed_at" in entry assert "title" in entry assert "contributor" in entry @@ -1492,6 +1515,8 @@ def test_container_creation_entry_returns_first_version(self): assert entry is not None assert entry["action"] == "created" assert entry["item_type"] == "unit" + assert entry["old_version"] == 0 + assert entry["new_version"] == 1 assert entry["title"] == "My Unit" assert "changed_at" in entry assert "contributor" in entry