Merge branch 'master' into issue-35278-unpin-charset-normalizer

This commit is contained in:
Edward Zarecor
2025-05-02 14:57:58 -04:00
committed by GitHub
19 changed files with 125 additions and 86 deletions

View File

@@ -354,7 +354,7 @@ class ContainerLink(EntityLinkBase):
@classmethod
def update_or_create(
cls,
upstream_container: Container | None,
upstream_container_id: int | None,
/,
upstream_container_key: LibraryContainerLocator,
upstream_context_key: str,
@@ -377,8 +377,8 @@ class ContainerLink(EntityLinkBase):
'version_synced': version_synced,
'version_declined': version_declined,
}
if upstream_container:
new_values['upstream_container'] = upstream_container
if upstream_container_id:
new_values['upstream_container_id'] = upstream_container_id
try:
link = cls.objects.get(downstream_usage_key=downstream_usage_key)
has_changes = False

View File

@@ -267,7 +267,7 @@ class DownstreamView(DeveloperErrorViewMixin, APIView):
fetch_customizable_fields_from_block(downstream=downstream, user=request.user)
else:
assert isinstance(link.upstream_key, LibraryContainerLocator)
fetch_customizable_fields_from_container(downstream=downstream, user=request.user)
fetch_customizable_fields_from_container(downstream=downstream)
except BadDownstream as exc:
logger.exception(
"'%s' is an invalid downstream; refusing to set its upstream to '%s'",

View File

@@ -87,7 +87,7 @@ from common.djangoapps.util.milestones_helpers import (
from common.djangoapps.xblock_django.api import deprecated_xblocks
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
from openedx.core import toggles as core_toggles
from openedx.core.djangoapps.content_libraries.api import get_container_from_key
from openedx.core.djangoapps.content_libraries.api import get_container
from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled
from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course
from openedx.core.djangoapps.discussions.config.waffle import ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND
@@ -2402,7 +2402,7 @@ def _create_or_update_container_link(course_key: CourseKey, created: datetime |
"""
upstream_container_key = LibraryContainerLocator.from_string(xblock.upstream)
try:
lib_component = get_container_from_key(upstream_container_key)
lib_component = get_container(upstream_container_key).container_pk
except ObjectDoesNotExist:
log.error(f"Library component not found for {upstream_container_key}")
lib_component = None

View File

@@ -45,7 +45,7 @@ def sync_from_upstream_container(
user,
permission=lib_api.permissions.CAN_VIEW_THIS_CONTENT_LIBRARY,
)
upstream_meta = lib_api.get_container(link.upstream_key, user)
upstream_meta = lib_api.get_container(link.upstream_key)
upstream_children = lib_api.get_container_children(link.upstream_key, published=True)
_update_customizable_fields(upstream=upstream_meta, downstream=downstream, only_fetch=False)
_update_non_customizable_fields(upstream=upstream_meta, downstream=downstream)
@@ -54,7 +54,7 @@ def sync_from_upstream_container(
return upstream_children
def fetch_customizable_fields_from_container(*, downstream: XBlock, user: User) -> None:
def fetch_customizable_fields_from_container(*, downstream: XBlock) -> None:
"""
Fetch upstream-defined value of customizable fields and save them on the downstream.
@@ -62,7 +62,7 @@ def fetch_customizable_fields_from_container(*, downstream: XBlock, user: User)
Basically, this sets the value of "upstream_display_name" on the downstream block.
"""
upstream = lib_api.get_container(LibraryContainerLocator.from_string(downstream.upstream), user)
upstream = lib_api.get_container(LibraryContainerLocator.from_string(downstream.upstream))
_update_customizable_fields(upstream=upstream, downstream=downstream, only_fetch=True)

View File

@@ -14,9 +14,20 @@ from openedx.core.lib.courses import course_image_url
<%inherit file="../main.html" />
<%block name="headextra">
## OG (Open Graph) title and description added below to give social media info to display
<%
site_domain = static.get_value('site_domain', settings.SITE_NAME)
site_protocol = 'https' if settings.HTTPS == 'on' else 'http'
og_img_url = "{protocol}://{domain}{path}".format(
protocol=site_protocol,
domain=site_domain,
path=course_image_urls['large']
)
%>
## OG (Open Graph) title, image and description added below to give social media info to display
## (https://developers.facebook.com/docs/opengraph/howtos/maximizing-distribution-media-content#tags)
<meta property="og:title" content="${course.display_name_with_default}" />
<meta property="og:image" content="${og_img_url}" />
<meta property="og:description" content="${get_course_about_section(request, course, 'short_description')}" />
</%block>

View File

@@ -71,6 +71,7 @@ class Fields:
# The "content" field is a dictionary of arbitrary data, depending on the block_type.
# It comes from each XBlock's index_dictionary() method (if present) plus some processing.
# Text (html) blocks have an "html_content" key in here, capa has "capa_content" and "problem_types", and so on.
# Containers store their list of child usage keys here.
content = "content"
# Collections use this field to communicate how many entities/components they contain.
@@ -87,6 +88,7 @@ class Fields:
published = "published"
published_display_name = "display_name"
published_description = "description"
published_content = "content"
published_num_children = "num_children"
# Note: new fields or values can be added at any time, but if they need to be indexed for filtering or keyword
@@ -347,13 +349,10 @@ def _collections_for_content_object(object_id: OpaqueKey) -> dict:
collections = authoring_api.get_entity_collections(
component.learning_package_id,
component.key,
)
).values('key', 'title')
elif isinstance(object_id, LibraryContainerLocator):
container = lib_api.get_container_from_key(object_id)
collections = authoring_api.get_entity_collections(
container.publishable_entity.learning_package_id,
container.key,
)
container = lib_api.get_container(object_id, include_collections=True)
collections = container.collections
else:
log.warning(f"Unexpected key type for {object_id}")
@@ -364,8 +363,8 @@ def _collections_for_content_object(object_id: OpaqueKey) -> dict:
return result
for collection in collections:
result[Fields.collections][Fields.collections_display_name].append(collection.title)
result[Fields.collections][Fields.collections_key].append(collection.key)
result[Fields.collections][Fields.collections_display_name].append(collection["title"])
result[Fields.collections][Fields.collections_key].append(collection["key"])
return result
@@ -581,9 +580,13 @@ def searchable_doc_for_container(
container = lib_api.get_container(container_key)
except lib_api.ContentLibraryContainerNotFound:
# Container not found, so we can only return the base doc
log.error(f"Container {container_key} not found")
return doc
draft_num_children = lib_api.get_container_children_count(container_key, published=False)
draft_children = lib_api.get_container_children(
container_key,
published=False,
)
publish_status = PublishStatus.published
if container.last_published is None:
publish_status = PublishStatus.never
@@ -594,7 +597,13 @@ def searchable_doc_for_container(
Fields.display_name: container.display_name,
Fields.created: container.created.timestamp(),
Fields.modified: container.modified.timestamp(),
Fields.num_children: draft_num_children,
Fields.num_children: len(draft_children),
Fields.content: {
"child_usage_keys": [
str(child.usage_key)
for child in draft_children
],
},
Fields.publish_status: publish_status,
Fields.last_published: container.last_published.timestamp() if container.last_published else None,
})
@@ -603,10 +612,19 @@ def searchable_doc_for_container(
doc[Fields.breadcrumbs] = [{"display_name": library.title}]
if container.published_version_num is not None:
published_num_children = lib_api.get_container_children_count(container_key, published=True)
published_children = lib_api.get_container_children(
container_key,
published=True,
)
doc[Fields.published] = {
Fields.published_num_children: published_num_children,
Fields.published_display_name: container.published_display_name,
Fields.published_num_children: len(published_children),
Fields.published_content: {
"child_usage_keys": [
str(child.usage_key)
for child in published_children
],
},
}
return doc

View File

@@ -238,6 +238,7 @@ class TestSearchApi(ModuleStoreTestCase):
"display_name": "Unit 1",
# description is not set for containers
"num_children": 0,
"content": {"child_usage_keys": []},
"publish_status": "never",
"context_key": "lib:org1:lib",
"org": "org1",

View File

@@ -531,6 +531,9 @@ class StudioDocumentsTest(SharedModuleStoreTestCase):
"display_name": "A Unit in the Search Index",
# description is not set for containers
"num_children": 0,
"content": {
"child_usage_keys": [],
},
"publish_status": "never",
"context_key": "lib:edX:2012_Fall",
"access_id": self.library_access_id,
@@ -571,6 +574,11 @@ class StudioDocumentsTest(SharedModuleStoreTestCase):
"display_name": "A Unit in the Search Index",
# description is not set for containers
"num_children": 1,
"content": {
"child_usage_keys": [
"lb:edX:2012_Fall:html:text2",
],
},
"publish_status": "published",
"context_key": "lib:edX:2012_Fall",
"access_id": self.library_access_id,
@@ -585,6 +593,11 @@ class StudioDocumentsTest(SharedModuleStoreTestCase):
"published": {
"num_children": 1,
"display_name": "A Unit in the Search Index",
"content": {
"child_usage_keys": [
"lb:edX:2012_Fall:html:text2",
],
},
},
}
@@ -627,6 +640,12 @@ class StudioDocumentsTest(SharedModuleStoreTestCase):
"display_name": "A Unit in the Search Index",
# description is not set for containers
"num_children": 2,
"content": {
"child_usage_keys": [
"lb:edX:2012_Fall:html:text2",
"lb:edX:2012_Fall:html:text3",
],
},
"publish_status": "modified",
"context_key": "lib:edX:2012_Fall",
"access_id": self.library_access_id,
@@ -641,6 +660,11 @@ class StudioDocumentsTest(SharedModuleStoreTestCase):
"published": {
"num_children": 1,
"display_name": "A Unit in the Search Index",
"content": {
"child_usage_keys": [
"lb:edX:2012_Fall:html:text2",
],
},
},
}

View File

@@ -26,8 +26,6 @@ class LibraryXBlockMetadata(PublishableItem):
Class that represents the metadata about an XBlock in a content library.
"""
usage_key: LibraryUsageLocatorV2
# TODO: move tags_count to LibraryItem as all objects under a library can be tagged.
tags_count: int = 0
@classmethod
def from_component(cls, library_key, component, associated_collections=None):

View File

@@ -181,7 +181,7 @@ def update_library_collection_items(
def set_library_item_collections(
library_key: LibraryLocatorV2,
publishable_entity: PublishableEntity,
entity_key: str,
*,
collection_keys: list[str],
created_by: int | None = None,
@@ -207,6 +207,11 @@ def set_library_item_collections(
assert content_library.learning_package_id
assert content_library.library_key == library_key
publishable_entity = authoring_api.get_publishable_entity_by_key(
content_library.learning_package_id,
key=entity_key,
)
# Note: Component.key matches its PublishableEntity.key
collection_qs = authoring_api.get_collections(content_library.learning_package_id).filter(
key__in=collection_keys

View File

@@ -43,7 +43,6 @@ __all__ = [
"ContainerMetadata",
"ContainerType",
# API methods
"get_container_from_key",
"get_container",
"create_container",
"get_container_children",
@@ -111,6 +110,7 @@ class ContainerMetadata(PublishableItem):
"""
container_key: LibraryContainerLocator
container_type: ContainerType
container_pk: int
published_display_name: str | None
@classmethod
@@ -139,6 +139,7 @@ class ContainerMetadata(PublishableItem):
return cls(
container_key=container_key,
container_type=container_type,
container_pk=container.pk,
display_name=draft.title,
created=container.created,
modified=draft.created,
@@ -173,7 +174,7 @@ def library_container_locator(
)
def get_container_from_key(container_key: LibraryContainerLocator, isDeleted=False) -> Container:
def _get_container_from_key(container_key: LibraryContainerLocator, isDeleted=False) -> Container:
"""
Internal method to fetch the Container object from its LibraryContainerLocator
@@ -192,11 +193,15 @@ def get_container_from_key(container_key: LibraryContainerLocator, isDeleted=Fal
raise ContentLibraryContainerNotFound
def get_container(container_key: LibraryContainerLocator, include_collections=False) -> ContainerMetadata:
def get_container(
container_key: LibraryContainerLocator,
*,
include_collections=False,
) -> ContainerMetadata:
"""
Get a container (a Section, Subsection, or Unit).
"""
container = get_container_from_key(container_key)
container = _get_container_from_key(container_key)
if include_collections:
associated_collections = authoring_api.get_entity_collections(
container.publishable_entity.learning_package_id,
@@ -268,7 +273,7 @@ def update_container(
"""
Update a container (e.g. a Unit) title.
"""
container = get_container_from_key(container_key)
container = _get_container_from_key(container_key)
library_key = container_key.lib_key
assert container.unit
@@ -297,7 +302,7 @@ def delete_container(
No-op if container doesn't exist or has already been soft-deleted.
"""
library_key = container_key.lib_key
container = get_container_from_key(container_key)
container = _get_container_from_key(container_key)
affected_collections = authoring_api.get_entity_collections(
container.publishable_entity.learning_package_id,
@@ -332,7 +337,7 @@ def restore_container(container_key: LibraryContainerLocator) -> None:
Restore the specified library container.
"""
library_key = container_key.lib_key
container = get_container_from_key(container_key, isDeleted=True)
container = _get_container_from_key(container_key, isDeleted=True)
affected_collections = authoring_api.get_entity_collections(
container.publishable_entity.learning_package_id,
@@ -372,12 +377,13 @@ def restore_container(container_key: LibraryContainerLocator) -> None:
def get_container_children(
container_key: LibraryContainerLocator,
*,
published=False,
) -> list[LibraryXBlockMetadata | ContainerMetadata]:
"""
Get the entities contained in the given container (e.g. the components/xblocks in a unit)
"""
container = get_container_from_key(container_key)
container = _get_container_from_key(container_key)
if container_key.container_type == ContainerType.Unit.value:
child_components = authoring_api.get_components_in_unit(container.unit, published=published)
return [LibraryXBlockMetadata.from_component(
@@ -399,7 +405,7 @@ def get_container_children_count(
"""
Get the count of entities contained in the given container (e.g. the components/xblocks in a unit)
"""
container = get_container_from_key(container_key)
container = _get_container_from_key(container_key)
return authoring_api.get_container_children_count(container, published=published)
@@ -414,7 +420,7 @@ def update_container_children(
"""
library_key = container_key.lib_key
container_type = container_key.container_type
container = get_container_from_key(container_key)
container = _get_container_from_key(container_key)
match container_type:
case ContainerType.Unit.value:
components = [get_component_from_usage_key(key) for key in children_ids] # type: ignore[arg-type]
@@ -459,7 +465,7 @@ def publish_container_changes(container_key: LibraryContainerLocator, user_id: i
Publish all unpublished changes in a container and all its child
containers/blocks.
"""
container = get_container_from_key(container_key)
container = _get_container_from_key(container_key)
library_key = container_key.lib_key
content_library = ContentLibrary.objects.get_by_key(library_key) # type: ignore[attr-defined]
learning_package = content_library.learning_package

View File

@@ -184,6 +184,7 @@ class LibraryItem:
created: datetime
modified: datetime
display_name: str
tags_count: int = 0
@dataclass(frozen=True, kw_only=True)

View File

@@ -265,14 +265,14 @@ class LibraryBlockCollectionsView(APIView):
request.user,
permissions.CAN_EDIT_THIS_CONTENT_LIBRARY
)
component = api.get_component_from_usage_key(key)
serializer = ContentLibraryItemCollectionsUpdateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
component = api.get_component_from_usage_key(key)
collection_keys = serializer.validated_data['collection_keys']
api.set_library_item_collections(
library_key=key.lib_key,
publishable_entity=component.publishable_entity,
entity_key=component.publishable_entity.key,
collection_keys=collection_keys,
created_by=request.user.id,
content_library=content_library,

View File

@@ -184,7 +184,7 @@ class LibraryContainerChildrenView(GenericAPIView):
request.user,
permissions.CAN_VIEW_THIS_CONTENT_LIBRARY,
)
child_entities = api.get_container_children(container_key, published)
child_entities = api.get_container_children(container_key, published=published)
if container_key.container_type == api.ContainerType.Unit.value:
data = serializers.LibraryXBlockMetadataSerializer(child_entities, many=True).data
else:
@@ -314,14 +314,13 @@ class LibraryContainerCollectionsView(GenericAPIView):
request.user,
permissions.CAN_EDIT_THIS_CONTENT_LIBRARY
)
container = api.get_container_from_key(container_key)
serializer = serializers.ContentLibraryItemCollectionsUpdateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
collection_keys = serializer.validated_data['collection_keys']
api.set_library_item_collections(
library_key=container_key.lib_key,
publishable_entity=container.publishable_entity,
entity_key=container_key.container_id,
collection_keys=collection_keys,
created_by=request.user.id,
content_library=content_library,

View File

@@ -138,6 +138,7 @@ class PublishableItemSerializer(serializers.Serializer):
"""
id = serializers.SerializerMethodField()
display_name = serializers.CharField()
tags_count = serializers.IntegerField(read_only=True)
last_published = serializers.DateTimeField(format=DATETIME_FORMAT, read_only=True)
published_by = serializers.CharField(read_only=True)
last_draft_created = serializers.DateTimeField(format=DATETIME_FORMAT, read_only=True)
@@ -149,7 +150,6 @@ class PublishableItemSerializer(serializers.Serializer):
# When creating a new XBlock in a library, the slug becomes the ID part of
# the definition key and usage key:
slug = serializers.CharField(write_only=True)
tags_count = serializers.IntegerField(read_only=True)
collections = CollectionMetadataSerializer(many=True, required=False)
can_stand_alone = serializers.BooleanField(read_only=True)

View File

@@ -547,10 +547,9 @@ class ContentLibraryCollectionsTest(ContentLibrariesRestApiTest, OpenEdxEventsTe
LIBRARY_COLLECTION_UPDATED.connect(collection_update_event_receiver)
assert not list(self.col2.entities.all())
component = api.get_component_from_usage_key(UsageKey.from_string(self.lib2_problem_block["id"]))
api.set_library_item_collections(
self.lib2.library_key,
component.publishable_entity,
library_key=self.lib2.library_key,
entity_key=component.publishable_entity.key,
collection_keys=[self.col2.key, self.col3.key],
)

View File

@@ -109,26 +109,5 @@ several INI files, each containing a single line in the form of ``USERNAME
--username=<username-of-learner-to-retire>
**************************************************
Using the Driver Scripts in an Automated Framework
**************************************************
At edX, we call the user retirement scripts from
`Jenkins <https://jenkins.io/>`_ jobs on one of our internal Jenkins
services. The user retirement driver scripts are intended to be agnostic
about which automation framework you use, but they were only fully tested
from Jenkins.
For more information about how we execute these scripts at edX, see the
following wiki articles:
* `User Retirement Jenkins Implementation <https://openedx.atlassian.net/wiki/spaces/PLAT/pages/704872737/User+Retirement+Jenkins+Implementation>`_
* `How to: retirement Jenkins jobs development and testing <https://openedx.atlassian.net/wiki/spaces/PLAT/pages/698221444/How+to+retirement+Jenkins+jobs+development+and+testing>`_
And check out the Groovy DSL files we use to seed these jobs:
* `platform/jobs/RetirementJobs.groovy in edx/jenkins-job-dsl <https://github.com/edx/jenkins-job-dsl/blob/master/platform/jobs/RetirementJobs.groovy>`_
* `platform/jobs/RetirementJobEdxTriggers.groovy in edx/jenkins-job-dsl <https://github.com/edx/jenkins-job-dsl/blob/master/platform/jobs/RetirementJobEdxTriggers.groovy>`_
.. include:: ../../../../links/links.rst

View File

@@ -49,27 +49,27 @@ possible states required by all members of the Open edX community.
This example state diagram outlines the pathways users follow throughout the
workflow:
.. digraph:: retirement_states_example
:align: center
.. graphviz::
digraph retirement_states_example {
ranksep = "0.3";
ranksep = "0.3";
node[fontname=Courier,fontsize=12,shape=box,group=main]
{ rank = same INIT[style=invis] PENDING }
INIT -> PENDING;
"..."[shape=none]
PENDING -> RETIRING_ENROLLMENTS -> ENROLLMENTS_COMPLETE -> RETIRING_FORUMS -> FORUMS_COMPLETE -> "..." -> COMPLETE;
node[fontname=Courier,fontsize=12,shape=box,group=main]
{ rank = same INIT[style=invis] PENDING }
INIT -> PENDING;
"..."[shape=none]
PENDING -> RETIRING_ENROLLMENTS -> ENROLLMENTS_COMPLETE -> RETIRING_FORUMS -> FORUMS_COMPLETE -> "..." -> COMPLETE;
node[group=""];
RETIRING_ENROLLMENTS -> ERRORED;
RETIRING_FORUMS -> ERRORED;
PENDING -> ABORTED;
node[group=""];
RETIRING_ENROLLMENTS -> ERRORED;
RETIRING_FORUMS -> ERRORED;
PENDING -> ABORTED;
subgraph cluster_terminal_states {
label = "Terminal States";
labelloc = b // put label at bottom
{rank = same ERRORED COMPLETE ABORTED}
}
subgraph cluster_terminal_states {
label = "Terminal States";
labelloc = b // put label at bottom
{rank = same ERRORED COMPLETE ABORTED}
}
}
Unless an error occurs internal to the user retirement tooling, a user's
retirement state should always land in one of the terminal states. At that

View File

@@ -20,8 +20,6 @@ retirement errored during forums retirement, so we manually reset their state
from ``ERRORED`` to ``ENROLLMENTS_COMPLETE``.
.. graphviz::
:align: center
digraph G {
//rankdir=LR; // Rank Direction Left to Right
ranksep = "0.3";