Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2u/course optimizer #35887

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions cms/djangoapps/contentstore/core/course_optimizer_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
"""
Logic for handling actions in Studio related to Course Optimizer.
"""

import json

from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import get_xblock
from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import usage_key_with_run


def generate_broken_links_descriptor(json_content, request_user):
"""
Returns a Data Transfer Object for frontend given a list of broken links.

json_content contains a list of [block_id, link, is_locked]
is_locked is true if the link is a studio link and returns 403 on request

** Example DTO structure **
{
'sections': [
{
'id': 'section_id',
'displayName': 'section name',
'subsections': [
{
'id': 'subsection_id',
'displayName': 'subsection name',
'units': [
{
'id': 'unit_id',
'displayName': 'unit name',
'blocks': [
{
'id': 'block_id',
'displayName': 'block name',
'url': 'url/to/block',
'brokenLinks: [],
'lockedLinks: [],
},
...,
]
},
...,
]
},
...,
]
},
...,
]
}
"""
xblock_node_tree = {} # tree representation of xblock relationships
xblock_dictionary = {} # dictionary of xblock attributes

for item in json_content:
block_id, link, *rest = item
is_locked_flag = bool(rest[0])

usage_key = usage_key_with_run(block_id)
block = get_xblock(usage_key, request_user)
_update_node_tree_and_dictionary(
block=block,
link=link,
is_locked=is_locked_flag,
node_tree=xblock_node_tree,
dictionary=xblock_dictionary
)

return _create_dto_from_node_tree_recursive(xblock_node_tree, xblock_dictionary)


def _update_node_tree_and_dictionary(block, link, is_locked, node_tree, dictionary):
"""
Inserts a block into the node tree and add its attributes to the dictionary.

** Example node tree structure **
{
'section_id1': {
'subsection_id1': {
'unit_id1': {
'block_id1': {},
'block_id2': {},
...,
},
'unit_id2': {
'block_id3': {},
...,
},
...,
},
...,
},
...,
}

** Example dictionary structure **
{
'xblock_id: {
'display_name': 'xblock name'
'category': 'html'
},
...,
}
"""
path = _get_node_path(block)
current_node = node_tree
xblock_id = ''

# Traverse the path and build the tree structure
for xblock in path:
xblock_id = xblock.location.block_id
dictionary.setdefault(xblock_id,
{
'display_name': xblock.display_name,
'category': getattr(xblock, 'category', ''),
}
)
# Sets new current node and creates the node if it doesn't exist
current_node = current_node.setdefault(xblock_id, {})

# Add block-level details for the last xblock in the path (URL and broken/locked links)
dictionary[xblock_id].setdefault('url',
f'/course/{block.course_id}/editor/{block.category}/{block.location}'
)
if is_locked:
dictionary[xblock_id].setdefault('locked_links', []).append(link)
else:
dictionary[xblock_id].setdefault('broken_links', []).append(link)


def _get_node_path(block):
"""
Retrieves the path frmo the course root node to a specific block, excluding the root.

** Example Path structure **
[chapter_node, sequential_node, vertical_node, html_node]
"""
path = []
current_node = block

while current_node.get_parent():
path.append(current_node)
current_node = current_node.get_parent()

return list(reversed(path))


CATEGORY_TO_LEVEL_MAP = {
"chapter": "sections",
"sequential": "subsections",
"vertical": "units"
}


def _create_dto_from_node_tree_recursive(xblock_node, xblock_dictionary):
"""
Recursively build the Data Transfer Object from the node tree and dictionary.
"""
# Exit condition when there are no more child nodes (at block level)
if not xblock_node:
return None

level = None
xblock_children = []

for xblock_id, node in xblock_node.items():
child_blocks = _create_dto_from_node_tree_recursive(node, xblock_dictionary)
xblock_data = xblock_dictionary.get(xblock_id, {})

xblock_entry = {
'id': xblock_id,
'displayName': xblock_data.get('display_name', ''),
}
if child_blocks == None: # Leaf node
level = 'blocks'
xblock_entry.update({
'url': xblock_data.get('url', ''),
'brokenLinks': xblock_data.get('broken_links', []),
'lockedLinks': xblock_data.get('locked_links', []),
})
else: # Non-leaf node
category = xblock_data.get('category', None)
level = CATEGORY_TO_LEVEL_MAP.get(category, None)
xblock_entry.update(child_blocks)

xblock_children.append(xblock_entry)

return {level: xblock_children} if level else None
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .advanced_settings import AdvancedSettingsFieldSerializer, CourseAdvancedSettingsSerializer
from .assets import AssetSerializer
from .authoring_grading import CourseGradingModelSerializer
from .course_optimizer import LinkCheckSerializer
from .tabs import CourseTabSerializer, CourseTabUpdateSerializer, TabIDLocatorSerializer
from .transcripts import TranscriptSerializer, YoutubeTranscriptCheckSerializer, YoutubeTranscriptUploadSerializer
from .xblock import XblockSerializer
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
API Serializers for Course Optimizer
"""

from rest_framework import serializers


class LinkCheckBlockSerializer(serializers.Serializer):
""" Serializer for broken links block model data """
id = serializers.CharField(required=True, allow_null=False, allow_blank=False)
displayName = serializers.CharField(required=True, allow_null=False, allow_blank=True)
url = serializers.CharField(required=True, allow_null=False, allow_blank=False)
brokenLinks = serializers.ListField(required=False)
lockedLinks = serializers.ListField(required=False)

class LinkCheckUnitSerializer(serializers.Serializer):
""" Serializer for broken links unit model data """
id = serializers.CharField(required=True, allow_null=False, allow_blank=False)
displayName = serializers.CharField(required=True, allow_null=False, allow_blank=True)
blocks = LinkCheckBlockSerializer(many=True)

class LinkCheckSubsectionSerializer(serializers.Serializer):
""" Serializer for broken links subsection model data """
id = serializers.CharField(required=True, allow_null=False, allow_blank=False)
displayName = serializers.CharField(required=True, allow_null=False, allow_blank=True)
units = LinkCheckUnitSerializer(many=True)

class LinkCheckSectionSerializer(serializers.Serializer):
""" Serializer for broken links section model data """
id = serializers.CharField(required=True, allow_null=False, allow_blank=False)
displayName = serializers.CharField(required=True, allow_null=False, allow_blank=True)
subsections = LinkCheckSubsectionSerializer(many=True)

class LinkCheckOutputSerializer(serializers.Serializer):
""" Serializer for broken links output model data """
sections = LinkCheckSectionSerializer(many=True)

class LinkCheckSerializer(serializers.Serializer):
""" Serializer for broken links """
LinkCheckStatus = serializers.CharField(required=True)
LinkCheckOutput = LinkCheckOutputSerializer(required=True)
14 changes: 13 additions & 1 deletion cms/djangoapps/contentstore/rest_api/v0/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@

from .views import (
AdvancedCourseSettingsView,
APIHeartBeatView,
AuthoringGradingView,
CourseTabSettingsView,
CourseTabListView,
CourseTabReorderView,
LinkCheckView,
LinkCheckStatusView,
TranscriptView,
YoutubeTranscriptCheckView,
YoutubeTranscriptUploadView,
APIHeartBeatView
)
from .views import assets
from .views import authoring_videos
Expand Down Expand Up @@ -102,4 +104,14 @@
fr'^youtube_transcripts/{settings.COURSE_ID_PATTERN}/upload?$',
YoutubeTranscriptUploadView.as_view(), name='cms_api_youtube_transcripts_upload'
),

# Course Optimizer
re_path(
fr'^link_check/{settings.COURSE_ID_PATTERN}$',
LinkCheckView.as_view(), name='link_check'
),
re_path(
fr'^link_check_status/{settings.COURSE_ID_PATTERN}$',
LinkCheckStatusView.as_view(), name='link_check_status'
),
]
3 changes: 2 additions & 1 deletion cms/djangoapps/contentstore/rest_api/v0/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
Views for v0 contentstore API.
"""
from .advanced_settings import AdvancedCourseSettingsView
from .api_heartbeat import APIHeartBeatView
from .authoring_grading import AuthoringGradingView
from .course_optimizer import LinkCheckView, LinkCheckStatusView
from .tabs import CourseTabSettingsView, CourseTabListView, CourseTabReorderView
from .transcripts import TranscriptView, YoutubeTranscriptCheckView, YoutubeTranscriptUploadView
from .api_heartbeat import APIHeartBeatView
Loading
Loading