Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6f86848
refactor: convert libraries API from attr.s to dataclass, fix types
bradenmacdonald Mar 12, 2025
0508e3b
fix: make corresponding updates to 'search' code
bradenmacdonald Mar 14, 2025
88ba74f
feat: use new version of openedx-learning with containers support
bradenmacdonald Mar 12, 2025
144ff01
temp: Use opencraft branch of opaquekeys
ChrisChV Mar 13, 2025
d125b04
refactor: Use LibraryElementKey instead of LibraryCollectionKey
ChrisChV Mar 13, 2025
0b8e12c
refactor: split libraries API & REST API up into smaller modules
bradenmacdonald Mar 14, 2025
9383d5f
feat: new REST API for units in content libraries
bradenmacdonald Mar 14, 2025
60169db
feat: python+REST API to get a unit
bradenmacdonald Mar 14, 2025
6a9a916
feat: auto-generate slug/key/ID from title of units
bradenmacdonald Mar 14, 2025
0352559
feat: generate search index documents for containers
bradenmacdonald Mar 14, 2025
cf96c6f
refactor: rename LibraryElementKey to LibraryItemKey
pomegranited Mar 15, 2025
9320592
fix: lint error
pomegranited Mar 15, 2025
d140dfe
Merge remote-tracking branch 'origin/master' into braden/units-api
pomegranited Mar 15, 2025
2468ad0
feat: adds new units to search index on create/update
pomegranited Mar 16, 2025
52f2822
fix: pylint
rpenido Mar 17, 2025
182c608
fix: temp requirement
rpenido Mar 17, 2025
9459aad
fix: search index container events/tasks
rpenido Mar 17, 2025
f2946bd
Merge branch 'master' into braden/units-api
rpenido Mar 17, 2025
8c6cbf0
Merge remote-tracking branch 'origin/master' into braden/units-api
pomegranited Mar 17, 2025
726ae82
feat: add get_library_container_usage_key to libraries API
pomegranited Mar 18, 2025
9b75fce
fix: index all containers during reindex_studio
pomegranited Mar 18, 2025
faa27e2
chore: bump openedx-events requirement
pomegranited Mar 18, 2025
1dbbabe
fix: address review comments
pomegranited Mar 18, 2025
556fb78
chore: bumps openedx-learning to 0.19.1
pomegranited Mar 19, 2025
6bdf39f
Merge remote-tracking branch 'origin/master' into braden/units-api
pomegranited Mar 19, 2025
7081488
fix: rename api method to library_container_locator
pomegranited Mar 21, 2025
42bccba
chore: bumps opaque-keys dependency
pomegranited Mar 24, 2025
2cd7532
Merge remote-tracking branch 'origin/master' into braden/units-api
pomegranited Mar 24, 2025
6cb8125
Merge remote-tracking branch 'origin/master' into braden/units-api
pomegranited Mar 25, 2025
0370a37
Merge remote-tracking branch 'origin/master' into braden/units-api
pomegranited Mar 26, 2025
e0f4380
test: fix misnamed unit_usage_key
pomegranited Mar 27, 2025
1ef3b1e
feat: adds APIs to update or delete a container (#757)
pomegranited Mar 27, 2025
6686902
Merge remote-tracking branch 'origin/master' into braden/units-api
pomegranited Mar 28, 2025
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
1 change: 1 addition & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1876,6 +1876,7 @@
"openedx_learning.apps.authoring.components",
"openedx_learning.apps.authoring.contents",
"openedx_learning.apps.authoring.publishing",
"openedx_learning.apps.authoring.units",
]


Expand Down
1 change: 1 addition & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3373,6 +3373,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
"openedx_learning.apps.authoring.components",
"openedx_learning.apps.authoring.contents",
"openedx_learning.apps.authoring.publishing",
"openedx_learning.apps.authoring.units",
]


Expand Down
4 changes: 2 additions & 2 deletions openedx/core/djangoapps/content/search/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ def index_collection_batch(batch, num_done, library_key) -> int:

# To reduce memory usage on large instances, split up the Collections into pages of 100 collections:
library = lib_api.get_library(lib_key)
collections = authoring_api.get_collections(library.learning_package.id, enabled=True)
collections = authoring_api.get_collections(library.learning_package_id, enabled=True)
num_collections = collections.count()
num_collections_done = 0
status_cb(f"{num_collections_done + 1}/{num_collections}. Now indexing collections in library {lib_key}")
Expand Down Expand Up @@ -711,7 +711,7 @@ def update_library_components_collections(
Because there may be a lot of components, we send these updates to Meilisearch in batches.
"""
library = lib_api.get_library(library_key)
components = authoring_api.get_collection_components(library.learning_package.id, collection_key)
components = authoring_api.get_collection_components(library.learning_package_id, collection_key)

paginator = Paginator(components, batch_size)
for page in paginator.page_range:
Expand Down
19 changes: 9 additions & 10 deletions openedx/core/djangoapps/content/search/tests/test_documents.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for the Studio content search documents (what gets stored in the index)
"""
from dataclasses import replace
from datetime import datetime, timezone
from organizations.models import Organization

Expand Down Expand Up @@ -427,18 +428,16 @@ def test_html_published_library_block(self):
}

# Verify publish status is set to modified
old_modified = self.library_block.modified
old_published = self.library_block.last_published
self.library_block.modified = datetime(2024, 4, 5, 6, 7, 8, tzinfo=timezone.utc)
self.library_block.last_published = datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc)
doc = searchable_doc_for_library_block(self.library_block)
doc.update(searchable_doc_tags(self.library_block.usage_key))
doc.update(searchable_doc_collections(self.library_block.usage_key))
library_block_modified = replace(
self.library_block,
modified=datetime(2024, 4, 5, 6, 7, 8, tzinfo=timezone.utc),
last_published=datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc),
)
doc = searchable_doc_for_library_block(library_block_modified)
doc.update(searchable_doc_tags(library_block_modified.usage_key))
doc.update(searchable_doc_collections(library_block_modified.usage_key))
assert doc["publish_status"] == "modified"

self.library_block.modified = old_modified
self.library_block.last_published = old_published

def test_collection_with_library(self):
doc = searchable_doc_for_collection(self.library.key, self.collection.key)
doc.update(searchable_doc_tags_for_collection(self.library.key, self.collection.key))
Expand Down
7 changes: 7 additions & 0 deletions openedx/core/djangoapps/content_libraries/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Python API for working with content libraries
"""
from .containers import *
from .libraries import *
from .blocks import *
from . import permissions
30 changes: 30 additions & 0 deletions openedx/core/djangoapps/content_libraries/api/blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Content libraries API methods related to XBlocks/Components.

These methods don't enforce permissions (only the REST APIs do).
"""
# pylint: disable=unused-import

# TODO: move all the API methods related to blocks and assets in here from 'libraries.py'
# TODO: use __all__ to limit what symbols are public.

from .libraries import (
LibraryXBlockMetadata,
LibraryXBlockStaticFile,
LibraryXBlockType,
get_library_components,
get_library_block,
set_library_block_olx,
library_component_usage_key,
get_component_from_usage_key,
validate_can_add_block_to_library,
create_library_block,
import_staged_content_from_user_clipboard,
get_or_create_olx_media_type,
delete_library_block,
restore_library_block,
get_library_block_static_asset_files,
add_library_block_static_asset_file,
delete_library_block_static_asset_file,
publish_component_changes,
)
113 changes: 113 additions & 0 deletions openedx/core/djangoapps/content_libraries/api/containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
API for containers (Sections, Subsections, Units) in Content Libraries
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from uuid import uuid4

from django.utils.text import slugify
from opaque_keys.edx.locator import (
LibraryLocatorV2,
LibraryContainerLocator,
)

from openedx_learning.api import authoring as authoring_api
from openedx_learning.api import authoring_models

from ..models import ContentLibrary
from .libraries import PublishableItem

# The public API is only the following symbols:
__all__ = [
"ContainerMetadata",
"create_container",
]


class ContainerType(Enum):
Unit = "unit"


@dataclass(frozen=True, kw_only=True)
class ContainerMetadata(PublishableItem):
"""
Class that represents the metadata about an XBlock in a content library.
"""
container_key: LibraryContainerLocator
container_type: ContainerType

@classmethod
def from_container(cls, library_key, container: authoring_models.Container, associated_collections=None):
"""
Construct a LibraryXBlockMetadata from a Component object.
"""
last_publish_log = container.versioning.last_publish_log

assert container.unit is not None
container_type = ContainerType.Unit

published_by = None
if last_publish_log and last_publish_log.published_by:
published_by = last_publish_log.published_by.username

draft = container.versioning.draft
published = container.versioning.published
last_draft_created = draft.created if draft else None
last_draft_created_by = draft.publishable_entity_version.created_by.username if draft else ""

return cls(
container_key=LibraryContainerLocator(
library_key,
container_type=container_type.value,
container_id=container.publishable_entity.key,
),
container_type=container_type,
display_name=draft.title,
created=container.created,
modified=draft.created,
draft_version_num=draft.version_num,
published_version_num=published.version_num if published else None,
last_published=None if last_publish_log is None else last_publish_log.published_at,
published_by=published_by or "",
last_draft_created=last_draft_created,
last_draft_created_by=last_draft_created_by,
has_unpublished_changes=authoring_api.contains_unpublished_changes(container.pk),
collections=associated_collections or [],
)


def create_container(
library_key: LibraryLocatorV2,
container_type: ContainerType,
slug: str | None,
title: str,
user_id: int | None,
) -> ContainerMetadata:
"""
Create a container (e.g. a Unit) in the specified content library.

It will initially be empty.
"""
assert isinstance(library_key, LibraryLocatorV2)
content_library = ContentLibrary.objects.get_by_key(library_key)
assert content_library.learning_package_id # Should never happen but we made this a nullable field so need to check
if slug is None:
# Automatically generate a slug. Append a random suffix so it should be unique.
slug = slugify(title, allow_unicode=True) + '-' + uuid4().hex[-6:]
# Make sure the slug is valid by first creating a key for the new container:
LibraryContainerLocator(library_key=library_key, container_type=container_type.value, container_id=slug)
# Then try creating the actual container:
match container_type:
case ContainerType.Unit:
container, _initial_version = authoring_api.create_unit_and_version(
content_library.learning_package_id,
key=slug,
title=title,
created=datetime.now(),
created_by=user_id,
)
case _:
raise ValueError(f"Invalid container type: {container_type}")
return ContainerMetadata.from_container(library_key, container)
Loading
Loading