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 42 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
2aeddf6
fix: created at none case (#36117)
rayzhou-bit Jan 15, 2025
8af3e02
test: refactor check_broken_links and call it with test successfully
jesperhodge Jan 16, 2025
bfca1d2
test: get happy path test to work once
jesperhodge Jan 16, 2025
eccbfcf
test: happy path successfully
jesperhodge Jan 16, 2025
296cd08
test: correct links written to file
jesperhodge Jan 16, 2025
3f989f2
test: remove unnecessary tempfile mocking
jesperhodge Jan 16, 2025
17e18a2
refactor: remove unused code
jesperhodge Jan 16, 2025
53ef706
fix: discussion
jesperhodge Jan 16, 2025
abb5df5
test: get view
jesperhodge Jan 15, 2025
302bd48
test: add validation tests and document how to test views
jesperhodge Jan 15, 2025
971f2ac
fix: lint
jesperhodge Jan 15, 2025
2867f8b
fix: serialization
jesperhodge Jan 16, 2025
9507b12
docs: improve how-to doc
jesperhodge Jan 17, 2025
85c14da
course_optimizer_provider tests (#36033)
rayzhou-bit Jan 17, 2025
2d310c3
feat: scan_course_for_links tests
rayzhou-bit Jan 22, 2025
124cb02
optimizer studio url tests (#36130)
rayzhou-bit Jan 22, 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
Empty file.
210 changes: 210 additions & 0 deletions cms/djangoapps/contentstore/core/course_optimizer_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
"""
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.

** Example json_content structure **
Note: is_locked is true if the link is a studio link and returns 403
[
['block_id_1', 'link_1', is_locked],
['block_id_1', 'link_2', is_locked],
['block_id_2', 'link_3', is_locked],
...
]

** 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)
xblock_node_tree, xblock_dictionary = _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_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': 'chapter'
},
'html_block_id': {
'display_name': 'xblock name',
'category': 'chapter',
'url': 'url_1',
'locked_links': [...],
'broken_links': [...]
}
...,
}
"""
updated_tree, updated_dictionary = node_tree, dictionary

path = _get_node_path(block)
current_node = updated_tree
xblock_id = ''

# Traverse the path and build the tree structure
for xblock in path:
xblock_id = xblock.location.block_id
updated_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)
updated_dictionary[xblock_id].setdefault('url',
f'/course/{block.course_id}/editor/{block.category}/{block.location}'
)
if is_locked:
updated_dictionary[xblock_id].setdefault('locked_links', []).append(link)
else:
updated_dictionary[xblock_id].setdefault('broken_links', []).append(link)

return updated_tree, updated_dictionary


def _get_node_path(block):
"""
Retrieves the path from 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_recursive(xblock_node, xblock_dictionary):
"""
Recursively build the Data Transfer Object by using
the structure from the node tree and data from the 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_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
Empty file.
Loading
Loading