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

Open
wants to merge 49 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 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
44330a7
feat: Efficient logic to create DTO for link_check_status api (#35966)
rayzhou-bit Dec 4, 2024
31d7714
feat: locked link (#35976)
rayzhou-bit Dec 15, 2024
760ce47
feat: send datetime (#36035)
rayzhou-bit Dec 16, 2024
1ff5507
fix: do not require output or error (#36052)
rayzhou-bit Dec 20, 2024
f49e4e9
fix: broken links not showing up
jesperhodge Jan 9, 2025
c1db6e7
feat: TNL-11812 no nested course optimizer functions
Jan 10, 2025
2bd6f9d
feat: stubbed course optimizer tests
bszabo Jan 12, 2025
8cdf77d
feat: TNL-11812 Use TestCase base class
Jan 12, 2025
241e09d
feat: TNL-11812 Try static substitution
Jan 12, 2025
bd279f6
feat: TNL-11812 msg for assert
Jan 12, 2025
270227d
fix: studio url evaluation (#36092)
rayzhou-bit Jan 14, 2025
b66abce
test: add test file to check API authorizations
jesperhodge Jan 14, 2025
569af8a
fix: created at none case (#36117)
rayzhou-bit Jan 15, 2025
02013af
test: refactor check_broken_links and call it with test successfully
jesperhodge Jan 16, 2025
48c1868
test: get happy path test to work once
jesperhodge Jan 16, 2025
3f49cd9
test: happy path successfully
jesperhodge Jan 16, 2025
043b63c
test: correct links written to file
jesperhodge Jan 16, 2025
0e4cc58
test: remove unnecessary tempfile mocking
jesperhodge Jan 16, 2025
58be197
refactor: remove unused code
jesperhodge Jan 16, 2025
c95a359
fix: discussion
jesperhodge Jan 16, 2025
9c899fd
test: get view
jesperhodge Jan 15, 2025
4299386
test: add validation tests and document how to test views
jesperhodge Jan 15, 2025
810c432
fix: lint
jesperhodge Jan 15, 2025
3a7ba8a
fix: serialization
jesperhodge Jan 16, 2025
4b078a7
docs: improve how-to doc
jesperhodge Jan 17, 2025
6d546cc
feat: course_optimizer_provider tests (#36033)
rayzhou-bit Jan 17, 2025
ba3441e
feat: scan_course_for_links tests
rayzhou-bit Jan 22, 2025
786afc9
feat: optimizer studio url tests (#36130)
rayzhou-bit Jan 22, 2025
8a585e9
feat: Bszabo/course optimizer tests (#36175)
bszabo Jan 31, 2025
7678256
chore: lint
rayzhou-bit Feb 3, 2025
412c72f
chore: lint cont
rayzhou-bit Feb 3, 2025
b6b2dd7
feat: add course optimizer to tools dropdown in Studio legacy
Jan 31, 2025
7578015
chore: lint and comments
rayzhou-bit Feb 3, 2025
6747424
chore: more comments and lints
rayzhou-bit Feb 3, 2025
acc2a1d
chore: lint
rayzhou-bit Feb 3, 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.
"""
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 is 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