Merge branch 'master' into issue-35278-unpin-charset-normalizer
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -184,6 +184,7 @@ class LibraryItem:
|
||||
created: datetime
|
||||
modified: datetime
|
||||
display_name: str
|
||||
tags_count: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user