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

fix LinkCheckCreatedAt set to None #36116

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3ab1d74
feat: init
rayzhou-bit Oct 23, 2024
f616fce
feat: apis
rayzhou-bit Nov 8, 2024
2fca01d
feat: url processing
rayzhou-bit Nov 19, 2024
712041e
chore: cleanup
rayzhou-bit Nov 20, 2024
355533b
feat: tasks code readability
rayzhou-bit Nov 21, 2024
e9fb603
feat: name and description changes to course opti
rayzhou-bit Nov 21, 2024
84cae82
feat: remove GET part of link_check
rayzhou-bit Nov 21, 2024
f53d578
feat: reorg code around status
rayzhou-bit Nov 21, 2024
0e41efb
feat: some code cleanup
rayzhou-bit Nov 25, 2024
233eb1f
feat: replace space with dash in status
rayzhou-bit Nov 25, 2024
fc021ee
feat: v0 rest_api wip
rayzhou-bit Dec 2, 2024
34ec30a
fix: remove code from old url code space
rayzhou-bit Dec 2, 2024
927b8c0
feat: messy new api wip
rayzhou-bit Dec 3, 2024
d125084
feat: make course optimizer scan only published version
jesperhodge Dec 3, 2024
6f98200
Efficient logic to create DTO for link_check_status api (#35966)
rayzhou-bit Dec 4, 2024
3f82c62
feat: locked link (#35976)
rayzhou-bit Dec 15, 2024
121210a
feat: send datetime (#36035)
rayzhou-bit Dec 16, 2024
162510b
fix: do not require output or error (#36052)
rayzhou-bit Dec 20, 2024
067e1b0
fix: broken links not showing up
jesperhodge Jan 9, 2025
51176cb
feat: TNL-11812 no nested course optimizer functions
Jan 10, 2025
8862d69
stubbed course optimizer tests
bszabo Jan 12, 2025
e4787e7
feat: TNL-11812 Use TestCase base class
Jan 12, 2025
e09781b
feat: TNL-11812 Try static substitution
Jan 12, 2025
de1aa1d
feat: TNL-11812 msg for assert
Jan 12, 2025
60947ce
fix: studio url evaluation (#36092)
rayzhou-bit Jan 14, 2025
4a2c148
test: add test file to check API authorizations
jesperhodge Jan 14, 2025
f86e02a
fix: created at none case
rayzhou-bit Jan 15, 2025
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
192 changes: 192 additions & 0 deletions cms/djangoapps/contentstore/core/course_optimizer_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""
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
if rest:
is_locked_flag = bool(rest[0])
else:
is_locked_flag = False

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,43 @@
"""
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)
LinkCheckCreatedAt = serializers.DateTimeField(required=False)
LinkCheckOutput = LinkCheckOutputSerializer(required=False)
LinkCheckError = serializers.CharField(required=False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from cms.djangoapps.contentstore.tests.test_utils import AuthorizeStaffTestCase
from rest_framework import status
from django.test import TestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from django.urls import reverse

class TestCourseOptimizer(AuthorizeStaffTestCase, ModuleStoreTestCase, TestCase):
'''
Tests for CourseOptimizer
'''
def test_inherited(self):
# This method ensures that pytest recognizes this class as containing tests
pass

def make_request(self, course_id=None, data=None):
return self.client.get(self.get_url(course_id), data)

def get_url(self, course_key):
return reverse(
'cms.djangoapps.contentstore:v0:link_check_status',
kwargs={'course_id': 'course-v1:someOrg+someCourse+someRun'}
)
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