Skip to content

Commit beaee85

Browse files
navinkarkeraUsamaSadiq
authored andcommitted
feat: api for adding, removing and updating components in container (#36434)
* feat: add components to container api * feat: remove and replace components in container api * refactor: container childern api * chore: fix lint issues * temp: install openedx-learning dev branch * feat: update publish_status and children count in index * chore: fix mypy issues * test: fix reindex test * refactor: rebase and fix conflicts * test: update test to check signals * docs: document can_stand_alone flag * chore: bump openedx-learning version
1 parent 58bd94e commit beaee85

File tree

16 files changed

+602
-57
lines changed

16 files changed

+602
-57
lines changed

openedx/core/djangoapps/content/search/documents.py

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import logging
77
from hashlib import blake2b
88

9-
from django.utils.text import slugify
109
from django.core.exceptions import ObjectDoesNotExist
10+
from django.utils.text import slugify
1111
from opaque_keys.edx.keys import LearningContextKey, UsageKey
12+
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2
1213
from openedx_learning.api import authoring as authoring_api
13-
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryContainerLocator
14+
from openedx_learning.api.authoring_models import Collection
1415
from rest_framework.exceptions import NotFound
1516

1617
from openedx.core.djangoapps.content.search.models import SearchAccess
@@ -19,7 +20,6 @@
1920
from openedx.core.djangoapps.content_tagging import api as tagging_api
2021
from openedx.core.djangoapps.xblock import api as xblock_api
2122
from openedx.core.djangoapps.xblock.data import LatestVersion
22-
from openedx_learning.api.authoring_models import Collection
2323

2424
log = logging.getLogger(__name__)
2525

@@ -554,7 +554,7 @@ def searchable_doc_for_container(
554554
) -> dict:
555555
"""
556556
Generate a dictionary document suitable for ingestion into a search engine
557-
like Meilisearch or Elasticsearch, so that the given collection can be
557+
like Meilisearch or Elasticsearch, so that the given container can be
558558
found using faceted search.
559559
560560
If no container is found for the given container key, the returned document
@@ -576,29 +576,33 @@ def searchable_doc_for_container(
576576

577577
try:
578578
container = lib_api.get_container(container_key)
579-
except lib_api.ContentLibraryCollectionNotFound:
579+
except lib_api.ContentLibraryContainerNotFound:
580580
# Container not found, so we can only return the base doc
581-
pass
581+
return doc
582582

583-
if container:
584-
# TODO: check if there's a more efficient way to load these num_children counts?
585-
draft_num_children = len(lib_api.get_container_children(container_key, published=False))
583+
draft_num_children = lib_api.get_container_children_count(container_key, published=False)
584+
publish_status = PublishStatus.published
585+
if container.last_published is None:
586+
publish_status = PublishStatus.never
587+
elif container.has_unpublished_changes:
588+
publish_status = PublishStatus.modified
586589

587-
doc.update({
588-
Fields.display_name: container.display_name,
589-
Fields.created: container.created.timestamp(),
590-
Fields.modified: container.modified.timestamp(),
591-
Fields.num_children: draft_num_children,
592-
})
593-
library = lib_api.get_library(container_key.library_key)
594-
if library:
595-
doc[Fields.breadcrumbs] = [{"display_name": library.title}]
596-
597-
if container.published_version_num is not None:
598-
published_num_children = len(lib_api.get_container_children(container_key, published=True))
599-
doc[Fields.published] = {
600-
# Fields.published_display_name: container_published.title, TODO: set the published title
601-
Fields.published_num_children: published_num_children,
602-
}
590+
doc.update({
591+
Fields.display_name: container.display_name,
592+
Fields.created: container.created.timestamp(),
593+
Fields.modified: container.modified.timestamp(),
594+
Fields.num_children: draft_num_children,
595+
Fields.publish_status: publish_status,
596+
})
597+
library = lib_api.get_library(container_key.library_key)
598+
if library:
599+
doc[Fields.breadcrumbs] = [{"display_name": library.title}]
600+
601+
if container.published_version_num is not None:
602+
published_num_children = lib_api.get_container_children_count(container_key, published=True)
603+
doc[Fields.published] = {
604+
# Fields.published_display_name: container_published.title, TODO: set the published title
605+
Fields.published_num_children: published_num_children,
606+
}
603607

604608
return doc

openedx/core/djangoapps/content/search/tests/test_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ def setUp(self):
227227
"display_name": "Unit 1",
228228
# description is not set for containers
229229
"num_children": 0,
230+
"publish_status": "never",
230231
"context_key": "lib:org1:lib",
231232
"org": "org1",
232233
"created": created_date.timestamp(),

openedx/core/djangoapps/content/search/tests/test_documents.py

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
"""
44
from dataclasses import replace
55
from datetime import datetime, timezone
6-
from organizations.models import Organization
76

87
from freezegun import freeze_time
8+
from openedx_learning.api import authoring as authoring_api
9+
from organizations.models import Organization
910

10-
from openedx.core.djangoapps.content_tagging import api as tagging_api
1111
from openedx.core.djangoapps.content_libraries import api as library_api
12+
from openedx.core.djangoapps.content_tagging import api as tagging_api
1213
from openedx.core.djangolib.testing.utils import skip_unless_cms
1314
from xmodule.modulestore.django import modulestore
1415
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
@@ -17,13 +18,13 @@
1718
try:
1819
# This import errors in the lms because content.search is not an installed app there.
1920
from ..documents import (
20-
searchable_doc_for_course_block,
21-
searchable_doc_tags,
22-
searchable_doc_tags_for_collection,
2321
searchable_doc_collections,
2422
searchable_doc_for_collection,
2523
searchable_doc_for_container,
24+
searchable_doc_for_course_block,
2625
searchable_doc_for_library_block,
26+
searchable_doc_tags,
27+
searchable_doc_tags_for_collection,
2728
)
2829
from ..models import SearchAccess
2930
except RuntimeError:
@@ -522,11 +523,112 @@ def test_draft_container(self):
522523
"display_name": "A Unit in the Search Index",
523524
# description is not set for containers
524525
"num_children": 0,
526+
"publish_status": "never",
527+
"context_key": "lib:edX:2012_Fall",
528+
"access_id": self.library_access_id,
529+
"breadcrumbs": [{"display_name": "some content_library"}],
530+
"created": 1680674828.0,
531+
"modified": 1680674828.0,
532+
# "tags" should be here but we haven't implemented them yet
533+
# "published" is not set since we haven't published it yet
534+
}
535+
536+
def test_published_container(self):
537+
"""
538+
Test creating a search document for a published container
539+
"""
540+
created_date = datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc)
541+
with freeze_time(created_date):
542+
container_meta = library_api.create_container(
543+
self.library.key,
544+
container_type=library_api.ContainerType.Unit,
545+
slug="unit1",
546+
title="A Unit in the Search Index",
547+
user_id=None,
548+
)
549+
library_api.update_container_children(
550+
container_meta.container_key,
551+
[self.library_block.usage_key],
552+
user_id=None,
553+
)
554+
library_api.publish_changes(self.library.key)
555+
556+
doc = searchable_doc_for_container(container_meta.container_key)
557+
558+
assert doc == {
559+
"id": "lctedx2012_fallunitunit1-edd13a0c",
560+
"block_id": "unit1",
561+
"block_type": "unit",
562+
"usage_key": "lct:edX:2012_Fall:unit:unit1",
563+
"type": "library_container",
564+
"org": "edX",
565+
"display_name": "A Unit in the Search Index",
566+
# description is not set for containers
567+
"num_children": 1,
568+
"publish_status": "published",
569+
"context_key": "lib:edX:2012_Fall",
570+
"access_id": self.library_access_id,
571+
"breadcrumbs": [{"display_name": "some content_library"}],
572+
"created": 1680674828.0,
573+
"modified": 1680674828.0,
574+
"published": {"num_children": 1},
575+
# "tags" should be here but we haven't implemented them yet
576+
# "published" is not set since we haven't published it yet
577+
}
578+
579+
def test_published_container_with_changes(self):
580+
"""
581+
Test creating a search document for a published container
582+
"""
583+
created_date = datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc)
584+
with freeze_time(created_date):
585+
container_meta = library_api.create_container(
586+
self.library.key,
587+
container_type=library_api.ContainerType.Unit,
588+
slug="unit1",
589+
title="A Unit in the Search Index",
590+
user_id=None,
591+
)
592+
library_api.update_container_children(
593+
container_meta.container_key,
594+
[self.library_block.usage_key],
595+
user_id=None,
596+
)
597+
library_api.publish_changes(self.library.key)
598+
block_2 = library_api.create_library_block(
599+
self.library.key,
600+
"html",
601+
"text3",
602+
)
603+
604+
# Add another component after publish
605+
with freeze_time(created_date):
606+
library_api.update_container_children(
607+
container_meta.container_key,
608+
[block_2.usage_key],
609+
user_id=None,
610+
entities_action=authoring_api.ChildrenEntitiesAction.APPEND,
611+
)
612+
613+
doc = searchable_doc_for_container(container_meta.container_key)
614+
615+
assert doc == {
616+
"id": "lctedx2012_fallunitunit1-edd13a0c",
617+
"block_id": "unit1",
618+
"block_type": "unit",
619+
"usage_key": "lct:edX:2012_Fall:unit:unit1",
620+
"type": "library_container",
621+
"org": "edX",
622+
"display_name": "A Unit in the Search Index",
623+
# description is not set for containers
624+
"num_children": 2,
625+
"publish_status": "modified",
525626
"context_key": "lib:edX:2012_Fall",
526627
"access_id": self.library_access_id,
527628
"breadcrumbs": [{"display_name": "some content_library"}],
528629
"created": 1680674828.0,
529630
"modified": 1680674828.0,
631+
"published": {"num_children": 1},
530632
# "tags" should be here but we haven't implemented them yet
531633
# "published" is not set since we haven't published it yet
532634
}

openedx/core/djangoapps/content_libraries/api/containers.py

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22
API for containers (Sections, Subsections, Units) in Content Libraries
33
"""
44
from __future__ import annotations
5+
56
from dataclasses import dataclass
67
from datetime import datetime
78
from enum import Enum
89
from uuid import uuid4
910

1011
from django.utils.text import slugify
1112
from opaque_keys.edx.locator import (
12-
LibraryLocatorV2,
1313
LibraryContainerLocator,
14+
LibraryLocatorV2,
15+
UsageKeyV2,
1416
)
15-
1617
from openedx_events.content_authoring.data import LibraryContainerData
1718
from openedx_events.content_authoring.signals import (
1819
LIBRARY_CONTAINER_CREATED,
@@ -22,8 +23,10 @@
2223
from openedx_learning.api import authoring as authoring_api
2324
from openedx_learning.api.authoring_models import Container
2425

26+
from openedx.core.djangoapps.xblock.api import get_component_from_usage_key
27+
2528
from ..models import ContentLibrary
26-
from .libraries import PublishableItem
29+
from .libraries import LibraryXBlockMetadata, PublishableItem
2730

2831

2932
# The public API is only the following symbols:
@@ -34,9 +37,11 @@
3437
"get_container",
3538
"create_container",
3639
"get_container_children",
40+
"get_container_children_count",
3741
"library_container_locator",
3842
"update_container",
3943
"delete_container",
44+
"update_container_children",
4045
]
4146

4247

@@ -252,14 +257,62 @@ def get_container_children(
252257
"""
253258
Get the entities contained in the given container (e.g. the components/xblocks in a unit)
254259
"""
255-
assert isinstance(container_key, LibraryContainerLocator)
256-
content_library = ContentLibrary.objects.get_by_key(container_key.library_key)
257-
learning_package = content_library.learning_package
258-
assert learning_package is not None
259-
container = authoring_api.get_container_by_key(
260-
learning_package.id,
261-
key=container_key.container_id,
260+
container = _get_container(container_key)
261+
if container_key.container_type == ContainerType.Unit.value:
262+
child_components = authoring_api.get_components_in_unit(container.unit, published=published)
263+
return [LibraryXBlockMetadata.from_component(
264+
container_key.library_key,
265+
entry.component
266+
) for entry in child_components]
267+
else:
268+
child_entities = authoring_api.get_entities_in_container(container, published=published)
269+
return [ContainerMetadata.from_container(
270+
container_key.library_key,
271+
entry.entity
272+
) for entry in child_entities]
273+
274+
275+
def get_container_children_count(
276+
container_key: LibraryContainerLocator,
277+
published=False,
278+
) -> int:
279+
"""
280+
Get the count of entities contained in the given container (e.g. the components/xblocks in a unit)
281+
"""
282+
container = _get_container(container_key)
283+
return authoring_api.get_container_children_count(container, published=published)
284+
285+
286+
def update_container_children(
287+
container_key: LibraryContainerLocator,
288+
children_ids: list[UsageKeyV2] | list[LibraryContainerLocator],
289+
user_id: int | None,
290+
entities_action: authoring_api.ChildrenEntitiesAction = authoring_api.ChildrenEntitiesAction.REPLACE,
291+
):
292+
"""
293+
Adds children components or containers to given container.
294+
"""
295+
library_key = container_key.library_key
296+
container_type = container_key.container_type
297+
container = _get_container(container_key)
298+
match container_type:
299+
case ContainerType.Unit.value:
300+
components = [get_component_from_usage_key(key) for key in children_ids] # type: ignore[arg-type]
301+
new_version = authoring_api.create_next_unit_version(
302+
container.unit,
303+
components=components, # type: ignore[arg-type]
304+
created=datetime.now(),
305+
created_by=user_id,
306+
entities_action=entities_action,
307+
)
308+
case _:
309+
raise ValueError(f"Invalid container type: {container_type}")
310+
311+
LIBRARY_CONTAINER_UPDATED.send_event(
312+
library_container=LibraryContainerData(
313+
library_key=library_key,
314+
container_key=str(container_key),
315+
)
262316
)
263-
child_entities = authoring_api.get_entities_in_container(container, published=published)
264-
# TODO: convert the return type to list[ContainerMetadata | LibraryXBlockMetadata] ?
265-
return child_entities
317+
318+
return ContainerMetadata.from_container(library_key, new_version.container)

0 commit comments

Comments
 (0)