Skip to content
Merged
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
2 changes: 1 addition & 1 deletion openedx_learning/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Open edX Learning ("Learning Core").
"""

__version__ = "0.21.0"
__version__ = "0.22.0"
47 changes: 46 additions & 1 deletion openedx_learning/apps/authoring/collections/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from ..publishing import api as publishing_api
from ..publishing.models import PublishableEntity
from .models import Collection
from .models import Collection, CollectionPublishableEntity

# The public API that will be re-exported by openedx_learning.apps.authoring.api
# is listed in the __all__ entries below. Internal helper functions that are
Expand All @@ -27,6 +27,7 @@
"remove_from_collection",
"restore_collection",
"update_collection",
"set_collections",
]


Expand Down Expand Up @@ -204,3 +205,47 @@ def get_collections(learning_package_id: int, enabled: bool | None = True) -> Qu
if enabled is not None:
qs = qs.filter(enabled=enabled)
return qs.select_related("learning_package").order_by('pk')


def set_collections(
publishable_entity: PublishableEntity,
collection_qset: QuerySet[Collection],
created_by: int | None = None,
) -> set[Collection]:
"""
Set collections for a given publishable entity.

These Collections must belong to the same LearningPackage as the PublishableEntity,
or a ValidationError will be raised.

Modified date of all collections related to entity is updated.

Returns the updated collections.
"""
# Disallow adding entities outside the collection's learning package
if collection_qset.exclude(learning_package_id=publishable_entity.learning_package_id).count():
raise ValidationError(
"Collection entities must be from the same learning package as the collection.",
)
current_relations = CollectionPublishableEntity.objects.filter(
entity=publishable_entity
).select_related('collection')
# Clear other collections for given entity and add only new collections from collection_qset
removed_collections = set(
r.collection for r in current_relations.exclude(collection__in=collection_qset)
)
new_collections = set(collection_qset.exclude(
id__in=current_relations.values_list('collection', flat=True)
))
# Triggers a m2m_changed signal
publishable_entity.collections.set(
objs=collection_qset,
through_defaults={"created_by_id": created_by},
)
# Update modified date via update to avoid triggering post_save signal for all collections, which can be very slow.
affected_collection = removed_collections | new_collections
Collection.objects.filter(
id__in=[collection.id for collection in affected_collection]
).update(modified=datetime.now(tz=timezone.utc))

return affected_collection
56 changes: 1 addition & 55 deletions openedx_learning/apps/authoring/components/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@
from __future__ import annotations

import mimetypes
from datetime import datetime, timezone
from datetime import datetime
from enum import StrEnum, auto
from logging import getLogger
from pathlib import Path
from uuid import UUID

from django.core.exceptions import ValidationError
from django.db.models import Q, QuerySet
from django.db.transaction import atomic
from django.http.response import HttpResponse, HttpResponseNotFound

from ..collections.models import Collection, CollectionPublishableEntity
from ..contents import api as contents_api
from ..publishing import api as publishing_api
from .models import Component, ComponentType, ComponentVersion, ComponentVersionContent
Expand All @@ -51,7 +49,6 @@
"look_up_component_version_content",
"AssetError",
"get_redirect_response_for_component_asset",
"set_collections",
]


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

return HttpResponse(headers={**info_headers, **redirect_headers})


def set_collections(
learning_package_id: int,
component: Component,
collection_qset: QuerySet[Collection],
created_by: int | None = None,
) -> set[Collection]:
"""
Set collections for a given component.

These Collections must belong to the same LearningPackage as the Component, or a ValidationError will be raised.

Modified date of all collections related to component is updated.

Returns the updated collections.
"""
# Disallow adding entities outside the collection's learning package
invalid_collection = collection_qset.exclude(learning_package_id=learning_package_id).first()
if invalid_collection:
raise ValidationError(
f"Cannot add collection {invalid_collection.pk} in learning package "
f"{invalid_collection.learning_package_id} to component {component} in "
f"learning package {learning_package_id}."
)
current_relations = CollectionPublishableEntity.objects.filter(
entity=component.publishable_entity
).select_related('collection')
# Clear other collections for given component and add only new collections from collection_qset
removed_collections = set(
r.collection for r in current_relations.exclude(collection__in=collection_qset)
)
new_collections = set(collection_qset.exclude(
id__in=current_relations.values_list('collection', flat=True)
))
# Use `remove` instead of `CollectionPublishableEntity.delete()` to trigger m2m_changed signal which will handle
# updating component index.
component.publishable_entity.collections.remove(*removed_collections)
component.publishable_entity.collections.add(
*new_collections,
through_defaults={"created_by_id": created_by},
)
# Update modified date via update to avoid triggering post_save signal for collections
# The signal triggers index update for each collection synchronously which will be very slow in this case.
# Instead trigger the index update in the caller function asynchronously.
affected_collection = removed_collections | new_collections
Collection.objects.filter(
id__in=[collection.id for collection in affected_collection]
).update(modified=datetime.now(tz=timezone.utc))

return affected_collection
16 changes: 16 additions & 0 deletions openedx_learning/apps/authoring/publishing/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"get_container",
"get_container_by_key",
"get_containers",
"get_collection_containers",
"ChildrenEntitiesAction",
"ContainerEntityListEntry",
"ContainerEntityRow",
Expand Down Expand Up @@ -954,6 +955,21 @@ def get_containers(
return container_cls.objects.filter(publishable_entity__learning_package=learning_package_id)


def get_collection_containers(
learning_package_id: int,
collection_key: str,
) -> QuerySet[Container]:
"""
Returns a QuerySet of Containers relating to the PublishableEntities in a Collection.

Containers have a one-to-one relationship with PublishableEntity, but the reverse may not always be true.
"""
return Container.objects.filter(
publishable_entity__learning_package_id=learning_package_id,
publishable_entity__collections__key=collection_key,
).order_by('pk')


@dataclass(frozen=True)
class ContainerEntityListEntry:
"""
Expand Down
Loading