Skip to content

Commit 9eadbc1

Browse files
authored
feat: add collections support for containers [FC-0083] (#299)
* refactor: move set_collection function to make it generic for any publishable entity * feat: add collections support for containers * chore: bump version
1 parent 2ce6a92 commit 9eadbc1

6 files changed

Lines changed: 225 additions & 139 deletions

File tree

openedx_learning/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Open edX Learning ("Learning Core").
33
"""
44

5-
__version__ = "0.21.0"
5+
__version__ = "0.22.0"

openedx_learning/apps/authoring/collections/api.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from ..publishing import api as publishing_api
1212
from ..publishing.models import PublishableEntity
13-
from .models import Collection
13+
from .models import Collection, CollectionPublishableEntity
1414

1515
# The public API that will be re-exported by openedx_learning.apps.authoring.api
1616
# is listed in the __all__ entries below. Internal helper functions that are
@@ -27,6 +27,7 @@
2727
"remove_from_collection",
2828
"restore_collection",
2929
"update_collection",
30+
"set_collections",
3031
]
3132

3233

@@ -204,3 +205,47 @@ def get_collections(learning_package_id: int, enabled: bool | None = True) -> Qu
204205
if enabled is not None:
205206
qs = qs.filter(enabled=enabled)
206207
return qs.select_related("learning_package").order_by('pk')
208+
209+
210+
def set_collections(
211+
publishable_entity: PublishableEntity,
212+
collection_qset: QuerySet[Collection],
213+
created_by: int | None = None,
214+
) -> set[Collection]:
215+
"""
216+
Set collections for a given publishable entity.
217+
218+
These Collections must belong to the same LearningPackage as the PublishableEntity,
219+
or a ValidationError will be raised.
220+
221+
Modified date of all collections related to entity is updated.
222+
223+
Returns the updated collections.
224+
"""
225+
# Disallow adding entities outside the collection's learning package
226+
if collection_qset.exclude(learning_package_id=publishable_entity.learning_package_id).count():
227+
raise ValidationError(
228+
"Collection entities must be from the same learning package as the collection.",
229+
)
230+
current_relations = CollectionPublishableEntity.objects.filter(
231+
entity=publishable_entity
232+
).select_related('collection')
233+
# Clear other collections for given entity and add only new collections from collection_qset
234+
removed_collections = set(
235+
r.collection for r in current_relations.exclude(collection__in=collection_qset)
236+
)
237+
new_collections = set(collection_qset.exclude(
238+
id__in=current_relations.values_list('collection', flat=True)
239+
))
240+
# Triggers a m2m_changed signal
241+
publishable_entity.collections.set(
242+
objs=collection_qset,
243+
through_defaults={"created_by_id": created_by},
244+
)
245+
# Update modified date via update to avoid triggering post_save signal for all collections, which can be very slow.
246+
affected_collection = removed_collections | new_collections
247+
Collection.objects.filter(
248+
id__in=[collection.id for collection in affected_collection]
249+
).update(modified=datetime.now(tz=timezone.utc))
250+
251+
return affected_collection

openedx_learning/apps/authoring/components/api.py

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,16 @@
1313
from __future__ import annotations
1414

1515
import mimetypes
16-
from datetime import datetime, timezone
16+
from datetime import datetime
1717
from enum import StrEnum, auto
1818
from logging import getLogger
1919
from pathlib import Path
2020
from uuid import UUID
2121

22-
from django.core.exceptions import ValidationError
2322
from django.db.models import Q, QuerySet
2423
from django.db.transaction import atomic
2524
from django.http.response import HttpResponse, HttpResponseNotFound
2625

27-
from ..collections.models import Collection, CollectionPublishableEntity
2826
from ..contents import api as contents_api
2927
from ..publishing import api as publishing_api
3028
from .models import Component, ComponentType, ComponentVersion, ComponentVersionContent
@@ -51,7 +49,6 @@
5149
"look_up_component_version_content",
5250
"AssetError",
5351
"get_redirect_response_for_component_asset",
54-
"set_collections",
5552
]
5653

5754

@@ -605,54 +602,3 @@ def _error_header(error: AssetError) -> dict[str, str]:
605602
)
606603

607604
return HttpResponse(headers={**info_headers, **redirect_headers})
608-
609-
610-
def set_collections(
611-
learning_package_id: int,
612-
component: Component,
613-
collection_qset: QuerySet[Collection],
614-
created_by: int | None = None,
615-
) -> set[Collection]:
616-
"""
617-
Set collections for a given component.
618-
619-
These Collections must belong to the same LearningPackage as the Component, or a ValidationError will be raised.
620-
621-
Modified date of all collections related to component is updated.
622-
623-
Returns the updated collections.
624-
"""
625-
# Disallow adding entities outside the collection's learning package
626-
invalid_collection = collection_qset.exclude(learning_package_id=learning_package_id).first()
627-
if invalid_collection:
628-
raise ValidationError(
629-
f"Cannot add collection {invalid_collection.pk} in learning package "
630-
f"{invalid_collection.learning_package_id} to component {component} in "
631-
f"learning package {learning_package_id}."
632-
)
633-
current_relations = CollectionPublishableEntity.objects.filter(
634-
entity=component.publishable_entity
635-
).select_related('collection')
636-
# Clear other collections for given component and add only new collections from collection_qset
637-
removed_collections = set(
638-
r.collection for r in current_relations.exclude(collection__in=collection_qset)
639-
)
640-
new_collections = set(collection_qset.exclude(
641-
id__in=current_relations.values_list('collection', flat=True)
642-
))
643-
# Use `remove` instead of `CollectionPublishableEntity.delete()` to trigger m2m_changed signal which will handle
644-
# updating component index.
645-
component.publishable_entity.collections.remove(*removed_collections)
646-
component.publishable_entity.collections.add(
647-
*new_collections,
648-
through_defaults={"created_by_id": created_by},
649-
)
650-
# Update modified date via update to avoid triggering post_save signal for collections
651-
# The signal triggers index update for each collection synchronously which will be very slow in this case.
652-
# Instead trigger the index update in the caller function asynchronously.
653-
affected_collection = removed_collections | new_collections
654-
Collection.objects.filter(
655-
id__in=[collection.id for collection in affected_collection]
656-
).update(modified=datetime.now(tz=timezone.utc))
657-
658-
return affected_collection

openedx_learning/apps/authoring/publishing/api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"get_container",
7575
"get_container_by_key",
7676
"get_containers",
77+
"get_collection_containers",
7778
"ChildrenEntitiesAction",
7879
"ContainerEntityListEntry",
7980
"ContainerEntityRow",
@@ -954,6 +955,21 @@ def get_containers(
954955
return container_cls.objects.filter(publishable_entity__learning_package=learning_package_id)
955956

956957

958+
def get_collection_containers(
959+
learning_package_id: int,
960+
collection_key: str,
961+
) -> QuerySet[Container]:
962+
"""
963+
Returns a QuerySet of Containers relating to the PublishableEntities in a Collection.
964+
965+
Containers have a one-to-one relationship with PublishableEntity, but the reverse may not always be true.
966+
"""
967+
return Container.objects.filter(
968+
publishable_entity__learning_package_id=learning_package_id,
969+
publishable_entity__collections__key=collection_key,
970+
).order_by('pk')
971+
972+
957973
@dataclass(frozen=True)
958974
class ContainerEntityListEntry:
959975
"""

0 commit comments

Comments
 (0)