Content API

Contents

Content API#

This is the public API for content authoring in the Open edX Core.

This is the single api module that code outside of the openedx_content.* package should import from. It will re-export the public functions from all api.py modules of its applets. It may also implement its own convenience APIs that wrap calls to multiple app APIs.

class openedx_content.api.AssetError(*values)#

Bases: StrEnum

Error codes related to fetching ComponentVersion assets.

class openedx_content.api.ChildrenEntitiesAction(*values)#

Bases: Enum

Possible actions for children entities

class openedx_content.api.ContainerEntityListEntry(entity_version: PublishableEntityVersion, pinned: bool)#

Bases: object

Data about a single entity in a container, e.g. a component in a unit.

__init__(entity_version: PublishableEntityVersion, pinned: bool) None#
exception openedx_content.api.ContainerImplementationMissingError#

Bases: Exception

Raised when trying to modify a container whose implementation [plugin] is no longer available.

class openedx_content.api.SectionListEntry(subsection_version: SubsectionVersion, pinned: bool = False)#

Bases: object

Data about a single subsection in a section.

__init__(subsection_version: SubsectionVersion, pinned: bool = False) None#
class openedx_content.api.SubsectionListEntry(unit_version: UnitVersion, pinned: bool = False)#

Bases: object

Data about a single unit in a subsection.

__init__(unit_version: UnitVersion, pinned: bool = False) None#
class openedx_content.api.UnitListEntry(component_version: ComponentVersion, pinned: bool = False)#

Bases: object

Data about a single entity in a container, e.g. a component in a unit.

__init__(component_version: ComponentVersion, pinned: bool = False) None#
openedx_content.api.add_to_collection(learning_package_id: ID, collection_code: str, entities_qset: QuerySet, created_by: int | None = None) Collection#

Adds a QuerySet of PublishableEntities to a Collection.

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

PublishableEntities already in the Collection are silently ignored.

The Collection object’s modified date is updated.

Returns the updated Collection object.

openedx_content.api.bulk_draft_changes_for(learning_package_id: ID, changed_by: int | None = None, changed_at: datetime | None = None) DraftChangeLogContext#

Context manager to do a single batch of Draft changes in.

Each publishable entity that is edited in this context will be tied to a single DraftChangeLogRecord, representing the cumulative changes made to that entity. Upon closing of the context, side effects of these changes will be calcuated, which may result in more DraftChangeLogRecords being created or updated. The resulting DraftChangeLogRecords and DraftChangeSideEffects will be tied together into a single DraftChangeLog, representing the collective changes to the learning package that happened in this context. All changes will be committed in a single atomic transaction.

Example:

with bulk_draft_changes_for(learning_package.id):
    for section in course:
        update_section_drafts(learning_package.id, section)

If you make a change to an entity without using this context manager, then the individual change (and its side effects) will be automatically wrapped in a one-off change context. For example, this:

update_one_component(component.learning_package, component)

is identical to this:

with bulk_draft_changes_for(component.learning_package.id):
    update_one_component(component.learning_package.id, component)
openedx_content.api.component_exists_by_code(learning_package_id: ID, /, namespace: str, type_name: str, component_code: str) bool#

Return True/False for whether a Component exists.

Note that a Component still exists even if it’s been soft-deleted (there’s no current Draft version for it), or if it’s been unpublished.

openedx_content.api.contains_unpublished_changes(container_or_pk: Container | ID, /) bool#

Check recursively if a container has any unpublished changes.

Note: I’ve preserved the API signature for now, but we probably eventually want to make a more general function that operates on PublishableEntities and dependencies, once we introduce those with courses and their files, grading policies, etc.

Note: unlike this method, the similar-sounding container.versioning.has_unpublished_changes property only reports if the container itself has unpublished changes, not if its contents do. So if you change a title or add a new child component, has_unpublished_changes will be True, but if you merely edit a component that’s in the container, it will be False. This method will return True in either case.

openedx_content.api.create_collection(learning_package_id: ID, collection_code: str, *, title: str, created_by: int | None, description: str = '', enabled: bool = True) Collection#

Create a new Collection

openedx_content.api.create_component(learning_package_id: ID, /, component_type: ComponentType, component_code: str, created: datetime, created_by: int | None, *, can_stand_alone: bool = True) Component#

Create a new Component (an entity like a Problem or Video).

The entity_ref is conventionally derived as "{namespace}:{type_name}:{component_code}", although callers should not assume that this will always be true.

openedx_content.api.create_component_and_version(learning_package_id: ID, /, component_type: ComponentType, component_code: str, title: str, created: datetime, created_by: int | None = None, *, can_stand_alone: bool = True, media: dict[str, ID | Media | bytes] | None = None) tuple[Component, ComponentVersion]#

Create a Component and associated ComponentVersion atomically.

openedx_content.api.create_component_version(component_id: ID, /, version_num: int, title: str, created: datetime, created_by: int | None, *, media: dict[str, ID | Media | bytes] | None = None) ComponentVersion#

Create a new ComponentVersion

The media parameter is a dict of file paths to Media-like things (a Media.ID, Media model object, or simple bytes). This is the Media that we want to associate with the new ComponentVersion. This will typically include a “block.xml” for the XBlock OLX definition, and possibly some static files like “static/diagram.png”.

Media can be specified as bytes for testing convenience, but you will almost always want to create a Media object first in actual app code, because that will give you better control over the MIME type and storage specifics (file vs. database).

openedx_content.api.create_container(learning_package_id: ID, container_code: str, created: datetime, created_by: int | None, *, container_cls: type[ContainerModel], can_stand_alone: bool = True) ContainerModel#

Create a new container.

Parameters:
  • learning_package_id – The ID of the learning package that contains the container.

  • container_code – A local slug identifier for the container, unique within the learning package (regardless of container type).

  • created – The date and time the container was created.

  • created_by – The ID of the user who created the container

  • container_cls – The subclass of container to create (e.g. Unit)

  • can_stand_alone – Set to False when created as part of containers

Returns:

The newly created container as an instance of container_cls.

openedx_content.api.create_container_and_version(learning_package_id: ID, container_code: str, *, title: str, container_cls: type[ContainerModel], entities: Iterable[PublishableEntity | PublishableEntityMixin | PublishableEntityVersion | PublishableEntityVersionMixin] | None = None, created: datetime, created_by: int | None = None, can_stand_alone: bool = True) tuple[ContainerModel, ContainerVersionModel]#
Parameters:
  • learning_package_id – The learning package ID.

  • container_code – A local slug identifier for the container, unique within the learning package (regardless of container type).

  • title – The title of the new container.

  • container_cls – The subclass of container to create (e.g. Unit)

  • entities – List of the entities that will comprise the entity list, in order. Pass PublishableEntityVersion or objects that use PublishableEntityVersionMixin to pin to a specific version. Pass PublishableEntity or objects that use PublishableEntityMixin for unpinned. Pass None for “no change”.

  • created – The creation date.

  • created_by – The ID of the user who created the container.

  • can_stand_alone – Set to False when created as part of containers

openedx_content.api.create_container_version(container_id: ID, version_num: int, *, title: str, entities: Iterable[PublishableEntity | PublishableEntityMixin | PublishableEntityVersion | PublishableEntityVersionMixin], created: datetime, created_by: int | None) ContainerVersion#

Create a new container version.

Parameters:
  • container_id – The ID of the container that the version belongs to.

  • version_num – The version number of the container.

  • title – The title of the container.

  • entities – List of the entities that will comprise the entity list, in order. Pass PublishableEntityVersion or objects that use PublishableEntityVersionMixin to pin to a specific version. Pass PublishableEntity or objects that use PublishableEntityMixin for unpinned.

  • created – The date and time the container version was created.

  • created_by – The ID of the user who created the container version.

Returns:

The newly created container version.

openedx_content.api.create_learning_package(package_ref: str, title: str, description: str = '', created: datetime | None = None) LearningPackage#

Create a new LearningPackage.

The package_ref must be unique.

Errors that can be raised:

  • django.core.exceptions.ValidationError

openedx_content.api.create_next_component_version(component_id: ID, /, media_to_replace: dict[str, ID | Media | bytes | None], created: datetime, title: str | None = None, created_by: int | None = None, *, force_version_num: int | None = None, ignore_previous_media: bool = False) ComponentVersion#

Create a new ComponentVersion based on the most recent version.

Parameters:
  • component_id (int) – The primary key of the Component to version.

  • media_to_replace (dict) – Mapping of file keys to Media IDs, None (for deletion), or bytes (for new file media).

  • created (datetime) – The creation timestamp for the new version.

  • title (str, optional) – Title for the new version. If None, uses the previous version’s title.

  • created_by (int, optional) – User ID of the creator.

  • force_version_num (int, optional) – If provided, overrides the automatic version number increment and sets this version’s number explicitly. Use this if you need to restore or import a version with a specific version number, such as during data migration or when synchronizing with external systems.

  • ignore_previous_media (bool) – If True, do not copy over media from the previous version.

Returns:

The newly created ComponentVersion instance.

Return type:

ComponentVersion

A very common pattern for making a new ComponentVersion is going to be “make it just like the last version, except changing these one or two things”. Before calling this, you should create any new media via the media API or send the media bytes as part of media_to_replace values.

The media_to_replace dict is a mapping of strings representing the local path/key for a file, to Media.id, Media object, or media bytes values. Passing media as bytes is useful for testing purposes, but you will almost always want to create a Media object first in actual app code, because that will give you better control over the resulting Media’s MIME type and storage specifics (file vs. database).

Using None for a value in this dict means to delete that key in the next version.

Make sure to wrap the function call on a atomic statement: with transaction.atomic():

It is okay to mark entries for deletion that don’t exist. For instance, if a version has a.txt and b.txt, sending a media_to_replace value of {"a.txt": None, "c.txt": None} will remove a.txt from the next version, leave b.txt alone, and will not error–even though there is no c.txt in the previous version. This is to make it a little more convenient to remove paths (e.g. due to deprecation) without having to always check for its existence first.

Why use force_version_num?

Normally, the version number is incremented automatically from the latest version. If you need to set a specific version number (for example, when restoring from backup, importing legacy data, or synchronizing with another system), use force_version_num to override the default behavior.

Why not use create_component_version?

The main reason is that we want to reuse the logic to create a static file component from a dictionary.

openedx_content.api.create_next_container_version(container: Container | ID, /, *, title: str | None = None, entities: Iterable[PublishableEntity | PublishableEntityMixin | PublishableEntityVersion | PublishableEntityVersionMixin] | None = None, created: datetime, created_by: int | None, entities_action: ChildrenEntitiesAction = ChildrenEntitiesAction.REPLACE, force_version_num: int | None = None) ContainerVersion#

Create the next version of a container. A new version of the container is created only when its metadata changes:

  • Something was added to the Container.

  • We re-ordered the rows in the container.

  • Something was removed from the container.

  • The Container’s metadata changed, e.g. the title.

  • We pin to different versions of the Container.

Parameters:
  • id – The ID of the container to create the next version of.

  • title – The title of the container. None to keep the current title.

  • entities – List of the entities that will comprise the entity list, in order. Pass PublishableEntityVersion or objects that use PublishableEntityVersionMixin to pin to a specific version. Pass PublishableEntity or objects that use PublishableEntityMixin for unpinned. Pass None for “no change”.

  • created – The date and time the container version was created.

  • created_by – The ID of the user who created the container version.

  • force_version_num (int, optional) – If provided, overrides the automatic version number increment and sets this version’s number explicitly. Use this if you need to restore or import a version with a specific version number, such as during data migration or when synchronizing with external systems.

Returns:

it will be a subclass of ContainerVersion

Return type:

The newly created container version. Note

Why use force_version_num?

Normally, the version number is incremented automatically from the latest version. If you need to set a specific version number (for example, when restoring from backup, importing legacy data, or synchronizing with another system), use force_version_num to override the default behavior.

openedx_content.api.create_next_section_version(section: Section | ID, *, title: str | None = None, subsections: Iterable[Subsection | SubsectionVersion] | None = None, created: datetime, created_by: int | None) SectionVersion#

See documentation of content_api.create_next_container_version()

The only real purpose of this function is to rename entities to subsections, and to specify that the version returned is a SectionVersion. In the future, if SectionVersion gets some fields that aren’t on ContainerVersion, this function would be more important.

openedx_content.api.create_next_subsection_version(subsection: Subsection | ID, *, title: str | None = None, units: Iterable[Unit | UnitVersion] | None = None, created: datetime, created_by: int | None) SubsectionVersion#

See documentation of content_api.create_next_container_version()

The only real purpose of this function is to rename entities to units, and to specify that the version returned is a SubsectionVersion. In the future, if SubsectionVersion gets some fields that aren’t on ContainerVersion, this function would be more important.

openedx_content.api.create_next_unit_version(unit: Unit | ID, *, title: str | None = None, components: Iterable[Component | ComponentVersion] | None = None, created: datetime, created_by: int | None) UnitVersion#

See documentation of content_api.create_next_container_version()

The only real purpose of this function is to rename entities to components, and to specify that the version returned is a UnitVersion. In the future, if UnitVersion gets some fields that aren’t on ContainerVersion, this function would be more important.

openedx_content.api.create_publishable_entity(learning_package_id: ID, /, entity_ref: str, created: datetime, created_by: int | None, *, can_stand_alone: bool = True) PublishableEntity#

Create a PublishableEntity.

You’d typically want to call this right before creating your own content model that points to it.

openedx_content.api.create_publishable_entity_version(entity_id: ID, /, version_num: int, title: str, created: datetime, created_by: int | None, *, dependencies: list[ID] | None = None) PublishableEntityVersion#

Create a PublishableEntityVersion.

You’d typically want to call this right before creating your own content version model that points to it.

openedx_content.api.create_section_and_version(learning_package_id: ID, container_code: str, *, title: str, subsections: Iterable[Subsection | SubsectionVersion] | None = None, created: datetime, created_by: int | None = None, can_stand_alone: bool = True) tuple[Section, SectionVersion]#

See documentation of content_api.create_container_and_version()

The only real purpose of this function is to rename entities to subsections, and to specify that the version returned is a SectionVersion. In the future, if SectionVersion gets some fields that aren’t on ContainerVersion, this function would be more important.

openedx_content.api.create_subsection_and_version(learning_package_id: ID, container_code: str, *, title: str, units: Iterable[Unit | UnitVersion] | None = None, created: datetime, created_by: int | None = None, can_stand_alone: bool = True) tuple[Subsection, SubsectionVersion]#

See documentation of content_api.create_container_and_version()

The only real purpose of this function is to rename entities to units, and to specify that the version returned is a SubsectionVersion. In the future, if SubsectionVersion gets some fields that aren’t on ContainerVersion, this function would be more important.

openedx_content.api.create_unit_and_version(learning_package_id: ID, container_code: str, *, title: str, components: Iterable[Component | ComponentVersion] | None = None, created: datetime, created_by: int | None = None, can_stand_alone: bool = True) tuple[Unit, UnitVersion]#

See documentation of content_api.create_container_and_version()

The only real purpose of this function is to rename entities to components, and to specify that the version returned is a UnitVersion. In the future, if UnitVersion gets some fields that aren’t on ContainerVersion, this function would be more important.

openedx_content.api.create_zip_file(package_ref: str, path: str, user: User | None = None, origin_server: str | None = None) None#

Creates a dump zip file for the given learning package key at the given path. The zip file contains a TOML representation of the learning package and its contents.

Can throw a NotFoundError at get_learning_package_by_ref

openedx_content.api.delete_collection(learning_package_id: ID, collection_code: str, *, hard_delete=False) Collection#

Disables or deletes a collection identified by the given learning_package + collection_code.

By default (hard_delete=False), the collection is “soft deleted”, i.e disabled. Soft-deleted collections can be re-enabled using restore_collection.

openedx_content.api.filter_publishable_entities(entities: QuerySet, has_draft=None, has_published=None) QuerySet#

Filter an entities query set.

has_draft: You can filter by entities that has a draft or not. has_published: You can filter by entities that has a published version or not.

openedx_content.api.get_all_container_subclasses() list[type[Container]]#

Get a list of installed Container types (Container subclasses).

openedx_content.api.get_collection(learning_package_id: ID, collection_code: str) Collection#

Get a Collection by ID

openedx_content.api.get_collection_components(learning_package_id: ID, collection_code: str) QuerySet#

Returns a QuerySet of Components relating to the PublishableEntities in a Collection.

Components have a one-to-one relationship with PublishableEntity, but the reverse may not always be true.

openedx_content.api.get_collection_entities(learning_package_id: ID, collection_code: str) QuerySet#

Returns a QuerySet of PublishableEntities in a Collection.

This is the same as collection.entities.all()

openedx_content.api.get_collections(learning_package_id: ID, enabled: bool | None = True) QuerySet#

Get all collections for a given learning package

Enabled collections are returned by default.

openedx_content.api.get_component(component_id: ID, /) Component#

Get Component by its primary key.

This is the same as the PublishableEntity’s ID primary key.

openedx_content.api.get_component_by_code(learning_package_id: ID, /, namespace: str, type_name: str, component_code: str) Component#

Get a Component by its unique (namespace, type, component_code) tuple.

openedx_content.api.get_components(learning_package_id: ID, /, draft: bool | None = None, published: bool | None = None, namespace: str | None = None, type_names: list[str] | None = None, draft_title: str | None = None, published_title: str | None = None) QuerySet#

Fetch a QuerySet of Components for a LearningPackage using various filters.

This method will pre-load all the relations that we need in order to get info from the Component’s draft and published versions, since we’ll be referencing these a lot.

openedx_content.api.get_components_in_unit(unit: Unit, *, published: bool) list[UnitListEntry]#

Get the list of entities and their versions in the draft or published version of the given Unit.

Parameters:
  • unit – The Unit, e.g. returned by get_unit()

  • publishedTrue if we want the published version of the unit, or False for the draft version.

openedx_content.api.get_container(pk: ID) Container#

Get a container by its primary key.

This returns the Container, not any specific version. It may not be published, or may have been soft deleted.

Parameters:

pk – The primary key of the container.

Returns:

The container with the given primary key.

openedx_content.api.get_container_by_code(learning_package_id: ID, /, container_code: str) Container#

Get a container by its learning package and container code.

Parameters:
  • learning_package_id – The ID of the learning package that contains the container.

  • container_code – The container code of the container.

Returns:

The container with the given container code (as Container, not as its typed subclass).

openedx_content.api.get_container_children_count(container: Container, *, published: bool)#

Get the count of entities in the current draft or published version of the given container.

Parameters:
  • container – The Container, e.g. returned by get_container()

  • publishedTrue if we want the published version of the container, or False for the draft version.

openedx_content.api.get_container_children_entity_refs(container_version: ContainerVersion) list[str]#

Fetch the list of entity refs for all entities in the given container version.

Parameters:

container_version – The ContainerVersion to fetch the entity refs for.

Returns:

A list of entity refs for all entities in the container version, ordered by position.

openedx_content.api.get_container_subclass(type_code: str, /) type[Container]#

Get subclass of Container from its type_code string (e.g. “unit”).

Will raise a ContainerImplementationMissingError if the type is not currently installed.

openedx_content.api.get_container_subclass_of(container: Container | ID, /) type[Container]#

Get the type of a container.

Works on either a generic Container instance or an instance of a specific subclass like Unit. Accepts an instance or an integer primary key.

Will raise a ContainerImplementationMissingError if the type is not currently installed.

openedx_content.api.get_container_type_code_of(container: Container | ID, /) str#

Get the type of a container, as a string - e.g. “unit”.

openedx_content.api.get_container_version(container_version_pk: int) ContainerVersion#

Get a container version by its primary key.

Parameters:

pk – The primary key of the container version.

Returns:

The container version with the given primary key.

openedx_content.api.get_containers(learning_package_id: ID, include_deleted: bool | None = False) QuerySet#

Get all containers in the given learning package.

Parameters:
  • learning_package_id – The primary key of the learning package

  • include_deleted – If True, include deleted containers (with no draft version) in the result.

Returns:

A queryset containing the container associated with the given learning package.

openedx_content.api.get_containers_with_entity(publishable_entity_pk: ID, *, ignore_pinned=False, published=False) QuerySet#

Find all draft containers that directly contain the given entity.

They will always be from the same learning package; cross-package containers are not allowed.

Parameters:
  • publishable_entity_pk – The ID of the PublishableEntity to search for.

  • ignore_pinned – if true, ignore any pinned references to the entity.

openedx_content.api.get_descendant_component_entity_ids(container: Container) list[int]#

Return the entity IDs of all leaf (non-Container) descendants of container.

Intermediate containers (e.g. Subsections, Units) are never included in the result; only leaf component entities are returned.

The traversal follows draft state only. Soft-deleted children are skipped automatically because get_entities_in_container omits them.

Edge cases:

  • A container whose draft was soft-deleted has no children to traverse and contributes no entity IDs.

  • An entity that appears as a child of multiple containers is deduplicated because the result is built from a set.

  • A cycle-guard (visited_container_pks) prevents infinite loops, which cannot occur in practice but is included for safety.

openedx_content.api.get_draft_version(publishable_entity_or_id: PublishableEntity | ID, /) PublishableEntityVersion | None#

Return current draft PublishableEntityVersion for this PublishableEntity.

This function will return None if there is no current draft.

openedx_content.api.get_entities_in_container(container: Container, *, published: bool, select_related_version: str | None = None) list[ContainerEntityListEntry]#

Get the list of entities and their versions in the current draft or published version of the given container.

Parameters:
  • container – The Container, e.g. returned by get_container()

  • publishedTrue if we want the published version of the container, or False for the draft version.

  • select_related_version – An optional optimization; specify a relationship

  • ContainerVersion (on)

  • containerversion__x (like componentversion or)

  • select_related. (to preload via)

openedx_content.api.get_entities_in_container_as_of(container: Container, publish_log_id: int) tuple[ContainerVersion | None, list[ContainerEntityListEntry]]#

Get the list of entities and their versions in the published version of the given container as of the given PublishLog version (which is essentially a version for the entire learning package).

Also returns the ContainerVersion so you can see the container title, settings?, and any other metadata from that point in time.

TODO: optimize, perhaps by having the publishlog store a record of all

ancestors of every modified PublishableEntity in the publish.

openedx_content.api.get_entities_with_unpublished_changes(learning_package_id: ID, /, include_deleted_drafts: bool = False) QuerySet#

Fetch entities that have unpublished changes.

By default, this excludes soft-deleted drafts but can be included using include_deleted_drafts option.

openedx_content.api.get_entities_with_unpublished_deletes(learning_package_id: ID, /) QuerySet#

Something will become “deleted” if it has a null Draft version but a not-null Published version. (If both are null, it means it’s already been deleted in a previous publish, or it was never published.)

openedx_content.api.get_entity_collections(learning_package_id: ID, entity_ref: str) QuerySet#

Get all collections in the given learning package which contain this entity.

Only enabled collections are returned.

openedx_content.api.get_entity_draft_history(publishable_entity_or_id: PublishableEntity | int, /) QuerySet#

Return DraftChangeLogRecords for a PublishableEntity since its last publication, ordered from most recent to oldest.

Edge cases:

  • Never published, no versions: returns an empty queryset.

  • Never published, has versions: returns all DraftChangeLogRecords.

  • No changes since the last publish: returns an empty queryset.

  • Last publish was a soft-delete (Published.version=None): the Published row still exists and its published_at timestamp is used as the lower bound, so only draft changes made after that soft-delete publish are returned. If there are no subsequent changes, the queryset is empty.

  • Unpublished soft-delete (soft-delete in draft, not yet published): the soft-delete DraftChangeLogRecord (new_version=None) is included because it was made after the last real publish.

openedx_content.api.get_entity_publish_history(publishable_entity_or_id: PublishableEntity | int, /) QuerySet#

Return all PublishLogRecords for a PublishableEntity, ordered most recent first.

Edge cases:

  • Never published: returns an empty queryset.

  • Soft-delete published (new_version=None): the record is included with old_version pointing to the last published version and new_version=None, indicating the entity was removed from the published state.

  • Multiple draft versions created between two publishes are compacted: each PublishLogRecord captures only the version that was actually published, not the intermediate draft versions.

openedx_content.api.get_entity_publish_history_entries(publishable_entity_or_id: PublishableEntity | int, /, publish_log_uuid: str) QuerySet#

Return the DraftChangeLogRecords associated with a specific PublishLog.

Finds the PublishLogRecord for the given entity and publish_log_uuid, then returns all DraftChangeLogRecords whose changed_at falls between the previous publish for this entity (exclusive) and this publish (inclusive), ordered most-recent-first.

Time bounds are used instead of version bounds because DraftChangeLogRecord has no single version_num field (soft-delete records have new_version=None), and using published_at timestamps cleanly handles all cases without extra joins.

Edge cases:

  • Each publish group is independent: only the DraftChangeLogRecords that belong to the requested publish_log_uuid are returned; changes attributed to other publish groups are excluded.

  • Soft-delete publish (PublishLogRecord.new_version=None): the soft-delete DraftChangeLogRecord (new_version=None) is included in the entries because it falls within the time window of that publish group.

Raises PublishLogRecord.DoesNotExist if publish_log_uuid is not found for this entity.

openedx_content.api.get_entity_version_contributors(publishable_entity_or_id: PublishableEntity | int, /, old_version_num: int, new_version_num: int | None) QuerySet#

Return distinct User queryset of contributors (changed_by) for DraftChangeLogRecords of a PublishableEntity after old_version_num.

If new_version_num is not None (normal publish), captures records where new_version is between old_version_num (exclusive) and new_version_num (inclusive).

If new_version_num is None (soft delete published), captures both normal edits after old_version_num AND the soft-delete record itself (identified by new_version=None and old_version >= old_version_num). A soft-delete record whose old_version falls before old_version_num is excluded.

Edge cases:

  • If no DraftChangeLogRecords fall in the range, returns an empty queryset.

  • Records with changed_by=None (system changes with no associated user) are always excluded.

  • A user who contributed multiple versions in the range appears only once (results are deduplicated with DISTINCT).

openedx_content.api.get_learning_package(learning_package_id: ID, /) LearningPackage#

Get LearningPackage by ID.

openedx_content.api.get_learning_package_by_ref(package_ref: str) LearningPackage#

Get LearningPackage by its package_ref.

Can throw a NotFoundError

openedx_content.api.get_media(media_id: int, /) Media#

Get a single Media object by its ID.

Media is always attached to something when it’s created, like to a ComponentVersion. That means the “right” way to access a Media is almost always going to be via those relations and not via this function. But I include this function anyway because it’s tiny to write and it’s better than someone using a get_or_create_* function when they really just want to get.

openedx_content.api.get_media_info_headers(media: Media) dict[str, str]#

Return HTTP headers that are specific to this Media.

This currently only consists of the Content-Type and ETag. These values are safe to cache.

openedx_content.api.get_or_create_component_type(namespace: str, name: str) ComponentType#

Get the ID of a ComponentType, and create if missing.

Caching Warning: Be careful about putting any caching decorator around this function (e.g. lru_cache). It’s possible that incorrect cache values could leak out in the event of a rollback–e.g. new types are introduced in a large import transaction which later fails. You can safely cache the results that come back from this function with a local dict in your import process instead.#

openedx_content.api.get_or_create_file_media(learning_package_id: ID, media_type_id: int, /, data: bytes, created: datetime) Media#

Get or create a Media with data stored in a file storage backend.

Use this function to store non-text data, large data, or data where low latency access is not necessary. Also use this function (or get_or_create_text_media with create_file=True) to store any Media that you want to be downloadable by browsers in the LMS, since the static asset serving system will only work with file-backed Media.

openedx_content.api.get_or_create_media_type(mime_type: str) MediaType#

Return the MediaType.id for the desired mime_type string.

If it is not found in the database, a new entry will be created for it. This lazy-writing means that MediaType entry IDs will not be the same across different server instances, and apps should not assume that will be the case. Even if we were to preload a bunch of common ones, we can’t anticipate the different XBlocks that will be installed in different server instances, each of which will use their own MediaType.

Caching Warning: Be careful about putting any caching decorator around this function (e.g. lru_cache). It’s possible that incorrect cache values could leak out in the event of a rollback–e.g. new types are introduced in a large import transaction which later fails. You can safely cache the results that come back from this function with a local dict in your import process instead.

openedx_content.api.get_or_create_text_media(learning_package_id: ID, media_type_id: int, /, text: str, created: datetime, create_file: bool = False) Media#

Get or create a Media entry with text data stored in the database.

Use this when you want to create relatively small chunks of text that need to be accessed quickly, especially if you’re pulling back multiple rows at once. For example, this is the function to call when storing OLX for a component XBlock like a ProblemBlock.

This function will always create a text entry in the database. In addition to this, if you specify create_file=True, it will also save a copy of that text data to the file storage backend. This is useful if we want to let that file be downloadable by browsers in the LMS at some point.

If you want to create a large text file, or want to create a text file that doesn’t need to be stored in the database, call create_file_content instead of this function.

openedx_content.api.get_publishable_entities(learning_package_id: ID, /) QuerySet#

Get all entities in a learning package.

openedx_content.api.get_published_version(publishable_entity_or_id: PublishableEntity | ID, /) PublishableEntityVersion | None#

Return current published PublishableEntityVersion for this PublishableEntity.

This function will return None if there is no current published version.

openedx_content.api.get_redirect_response_for_component_asset(component_version_uuid: UUID, asset_path: Path, public: bool = False) HttpResponse#

HttpResponse for a reverse-proxy to serve a ComponentVersion asset.

Parameters:
  • component_version_uuidUUID of the ComponentVersion that the asset is part of.

  • asset_path – Path to the asset being requested.

  • public – Is this asset going to be made available without auth checks? If True, this will return an HttpResponse that can be cached in a CDN and shared across many clients.

Response Codes

If the asset exists for this ComponentVersion, this function will return an HttpResponse with a status code of 200.

If the specified asset does not exist for this ComponentVersion, or if the ComponentVersion itself does not exist, the response code will be 404.

This function does not do auth checking of any sort. It will never return a 401 or 403 response code. That is by design. Figuring out who is making the request and whether they have permission to do so is the responsiblity of whatever is calling this function.

Metadata Headers

The HttpResponse returned by this function will have headers describing the asset and the ComponentVersion it belongs to (if it exists):

  • Content-Type

  • Etag (this will be the asset’s hash digest)

  • X-Open-edX-Component-Key

  • X-Open-edX-Component-Uuid

  • X-Open-edX-Component-Version-Uuid

  • X-Open-edX-Component-Version-Num

  • X-Open-edX-Learning-Package-Key

  • X-Open-edX-Learning-Package-Uuid

Asset Redirection

For performance reasons, the HttpResponse object returned by this function does not contain the actual media data of the asset. It requires an appropriately configured reverse proxy server that handles the X-Accel-Redirect header (both Caddy and Nginx support this).

Warning

If you add any headers here, you may need to add them in the “media” service container’s reverse proxy configuration. In Tutor, this is a Caddyfile. All non-standard HTTP headers should be prefixed with X-Open-edX-.

openedx_content.api.get_section(section_id: ID, /)#

Get a section

openedx_content.api.get_subsection(subsection_id: ID, /)#

Get a subsection

openedx_content.api.get_subsections_in_section(section: Section, *, published: bool) list[SectionListEntry]#

Get the list of entities and their versions in the draft or published version of the given Section.

Parameters:
  • section – The section, e.g. returned by get_section()

  • publishedTrue if we want the published version of the section, or False for the draft version.

openedx_content.api.get_unit(unit_id: ID, /)#

Get a unit

openedx_content.api.get_units_in_subsection(subsection: Subsection, *, published: bool) list[SubsectionListEntry]#

Get the list of entities and their versions in the draft or published version of the given Subsection.

Parameters:
  • subsection – The Subsection, e.g. returned by get_subsection()

  • publishedTrue if we want the published version of the subsection, or False for the draft version.

openedx_content.api.learning_package_exists(package_ref: str) bool#

Check whether a LearningPackage with a particular package_ref exists.

openedx_content.api.load_learning_package(path: str, package_ref: str | None = None, user: User | None = None) dict#

Loads a learning package from a zip file at the given path. Restores the learning package and its contents to the database. Returns a dictionary with the status of the operation and any errors encountered.

openedx_content.api.look_up_component_version_media(learning_package_ref: str, entity_ref: str, version_num: int, path: Path) ComponentVersionMedia#

Look up ComponentVersionMedia by human readable identifiers.

Can raise a django.core.exceptions.ObjectDoesNotExist error if there is no matching ComponentVersionMedia.

This API call was only used in our proof-of-concept assets media server, and I don’t know if we wantto make it a part of the public interface.

openedx_content.api.publish_all_drafts(learning_package_id: ID, /, message='', published_at: datetime | None = None, published_by: int | None = None) PublishLog#

Publish everything that is a Draft and is not already published.

openedx_content.api.publish_from_drafts(learning_package_id: ID, /, draft_qset: QuerySet, message: str = '', published_at: datetime | None = None, published_by: int | None = None, *, publish_dependencies: bool = True) PublishLog#

Publish the rows in the draft_model_qsets args passed in.

By default, this will also publish all dependencies (e.g. unpinned children) of the Drafts that are passed in.

openedx_content.api.register_publishable_models(content_model_cls: type[PublishableEntityMixin], content_version_model_cls: type[PublishableEntityVersionMixin]) PublishableContentModelRegistry#

Register what content model maps to what content version model.

This is so that we can provide convenience links between content models and content version models through the publishing apps, so that you can do things like finding the draft version of a content model more easily. See the publishable_entity.py module for more details.

This should only be imported and run from the your app’s AppConfig.ready() method. For example, in the components app, this looks like:

def ready(self):

from ..publishing.api import register_publishable_models from .models import Component, ComponentVersion

register_publishable_models(Component, ComponentVersion)

There may be a more clever way to introspect this information from the model metadata, but this is simple and explicit.

openedx_content.api.remove_from_collection(learning_package_id: ID, collection_code: str, entities_qset: QuerySet) Collection#

Removes a QuerySet of PublishableEntities from a Collection.

PublishableEntities are deleted (in bulk).

The Collection’s modified date is updated (even if nothing was removed).

Returns the updated Collection.

openedx_content.api.reset_drafts_to_published(learning_package_id: ID, /, reset_at: datetime | None = None, reset_by: int | None = None) None#

Reset all Drafts to point to the most recently Published versions.

This is a way to say “discard my unpublished changes” at the level of an entire LearningPackage. Note that the PublishableEntityVersions that were created in the mean time are not deleted.

Let’s look at the example of a PublishableEntity where Draft and Published both point to version 4.

  • The PublishableEntity is then edited multiple times, creating a new version with every save. Each new save also updates the the Draft to point to it. After three such saves, Draft points at version 7.

  • No new publishes have happened, so Published still points to version 4.

  • reset_drafts_to_published is called. Draft now points to version 4 to match Published.

  • The PublishableEntity is edited again. This creates version 8, and Draft now points to this new version.

So in the above example, versions 5-7 aren’t discarded from the history, and it’s important that the code creating the “next” version_num looks at the latest version created for a PublishableEntity (its latest attribute), rather than basing it off of the version that Draft points to.

openedx_content.api.restore_collection(learning_package_id: ID, collection_code: str) Collection#

Undo a “soft delete” by re-enabling a Collection.

openedx_content.api.set_collections(publishable_entity: PublishableEntity, collection_qset: QuerySet, 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.

openedx_content.api.set_draft_version(draft_or_id: Draft | ID, publishable_entity_version_pk: int | None, /, set_at: datetime | None = None, set_by: int | None = None) None#

Modify the Draft of a PublishableEntity to be a PublishableEntityVersion.

The draft argument can be either a Draft model object, or the primary key of a Draft/PublishableEntity (Draft is defined so these will be the same value).

This would most commonly be used to set the Draft to point to a newly created PublishableEntityVersion that was created in Studio (because someone edited some content). Setting a Draft’s version to None is like deleting it from Studio’s editing point of view (see soft_delete_draft for more details).

Calling this function attaches a new DraftChangeLogRecord and attaches it to a DraftChangeLog.

This function will create DraftSideEffect entries and properly add any containers that may have been affected by this draft update, UNLESS it is called from within a bulk_draft_changes_for block. If it is called from inside a bulk_draft_changes_for block, it will not add side-effects for containers, as bulk_draft_changes_for will automatically do that when the block exits.

openedx_content.api.soft_delete_draft(publishable_entity_id: ID, /, deleted_by: int | None = None) None#

Sets the Draft version to None.

This “deletes” the PublishableEntity from the point of an authoring environment like Studio, but doesn’t immediately remove the Published version. No version data is actually deleted, so restoring is just a matter of pointing the Draft back to the most recent PublishableEntityVersion for a given PublishableEntity.

openedx_content.api.update_collection(learning_package_id: ID, collection_code: str, *, title: str | None = None, description: str | None = None) Collection#

Update a Collection identified by the learning_package_id + collection_code.

openedx_content.api.update_learning_package(learning_package_id: ID, /, package_ref: str | None = None, title: str | None = None, description: str | None = None, updated: datetime | None = None) LearningPackage#

Make an update to LearningPackage metadata.

Note that LearningPackage itself is not versioned (only stuff inside it is).