Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
de29ef94
Commit
de29ef94
authored
Mar 01, 2017
by
Mushtaq Ali
Committed by
GitHub
Mar 01, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #14457 from edx/mushtaq/restrict-move-action
Allow move for content experiment
parents
e9b8e17f
0df36241
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
674 additions
and
88 deletions
+674
-88
cms/djangoapps/contentstore/utils.py
+17
-0
cms/djangoapps/contentstore/views/item.py
+33
-6
cms/djangoapps/contentstore/views/preview.py
+1
-0
cms/djangoapps/contentstore/views/tests/test_container_page.py
+42
-2
cms/djangoapps/contentstore/views/tests/test_item.py
+0
-0
cms/djangoapps/contentstore/views/tests/utils.py
+35
-21
cms/static/js/spec/views/move_xblock_spec.js
+114
-12
cms/static/js/views/modals/move_xblock_modal.js
+21
-2
cms/static/js/views/move_xblock_breadcrumb.js
+1
-5
cms/static/js/views/move_xblock_list.js
+25
-24
cms/static/sass/views/_container.scss
+7
-6
cms/templates/js/move-xblock-list.underscore
+14
-7
cms/templates/studio_xblock_wrapper.html
+2
-1
common/lib/xmodule/xmodule/library_content_module.py
+1
-0
common/lib/xmodule/xmodule/library_root_xblock.py
+1
-0
common/test/acceptance/pages/studio/container.py
+53
-2
common/test/acceptance/pages/studio/move_xblock.py
+78
-0
common/test/acceptance/tests/studio/test_studio_container.py
+229
-0
No files found.
cms/djangoapps/contentstore/utils.py
View file @
de29ef94
...
...
@@ -283,6 +283,23 @@ def reverse_usage_url(handler_name, usage_key, kwargs=None):
return
reverse_url
(
handler_name
,
'usage_key_string'
,
usage_key
,
kwargs
)
def
get_group_display_name
(
user_partitions
,
xblock_display_name
):
"""
Get the group name if matching group xblock is found.
Arguments:
user_partitions (Dict): Locator of source item.
xblock_display_name (String): Display name of group xblock.
Returns:
group name (String): Group name of the matching group.
"""
for
user_partition
in
user_partitions
:
for
group
in
user_partition
[
'groups'
]:
if
str
(
group
[
'id'
])
in
xblock_display_name
:
return
group
[
'name'
]
def
get_user_partition_info
(
xblock
,
schemes
=
None
,
course
=
None
):
"""
Retrieve user partition information for an XBlock for display in editors.
...
...
cms/djangoapps/contentstore/views/item.py
View file @
de29ef94
...
...
@@ -29,7 +29,7 @@ from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
from
contentstore.utils
import
(
find_release_date_source
,
find_staff_lock_source
,
is_currently_visible_to_students
,
ancestor_has_staff_lock
,
has_children_visible_to_specific_content_groups
,
get_user_partition_info
,
get_user_partition_info
,
get_group_display_name
,
)
from
contentstore.views.helpers
import
is_unit
,
xblock_studio_url
,
xblock_primary_child_category
,
\
xblock_type_display_name
,
get_parent_xblock
,
create_xblock
,
usage_key_with_run
...
...
@@ -675,6 +675,21 @@ def _get_source_index(source_usage_key, source_parent):
return
None
def
is_source_item_in_target_parents
(
source_item
,
target_parent
):
"""
Returns True if source item is found in target parents otherwise False.
Arguments:
source_item (XBlock): Source Xblock.
target_parent (XBlock): Target XBlock.
"""
target_ancestors
=
_create_xblock_ancestor_info
(
target_parent
,
is_concise
=
True
)[
'ancestors'
]
for
target_ancestor
in
target_ancestors
:
if
unicode
(
source_item
.
location
)
==
target_ancestor
[
'id'
]:
return
True
return
False
def
_move_item
(
source_usage_key
,
target_parent_usage_key
,
user
,
target_index
=
None
):
"""
Move an existing xblock as a child of the supplied target_parent_usage_key.
...
...
@@ -688,8 +703,11 @@ def _move_item(source_usage_key, target_parent_usage_key, user, target_index=Non
JsonResponse: Information regarding move operation. It may contains error info if an invalid move operation
is performed.
"""
# Get the list of all component type XBlocks
component_types
=
sorted
(
set
(
name
for
name
,
class_
in
XBlock
.
load_classes
())
-
set
(
DIRECT_ONLY_CATEGORIES
))
# Get the list of all parentable component type XBlocks.
parent_component_types
=
list
(
set
(
name
for
name
,
class_
in
XBlock
.
load_classes
()
if
getattr
(
class_
,
'has_children'
,
False
))
-
set
(
DIRECT_ONLY_CATEGORIES
)
)
store
=
modulestore
()
with
store
.
bulk_operations
(
source_usage_key
.
course_key
):
...
...
@@ -705,18 +723,22 @@ def _move_item(source_usage_key, target_parent_usage_key, user, target_index=Non
source_index
=
_get_source_index
(
source_usage_key
,
source_parent
)
valid_move_type
=
{
'vertical'
:
source_type
if
source_type
in
component_types
else
'component'
,
'sequential'
:
'vertical'
,
'chapter'
:
'sequential'
,
}
if
valid_move_type
.
get
(
target_parent_type
,
''
)
!=
source_type
:
if
(
valid_move_type
.
get
(
target_parent_type
,
''
)
!=
source_type
and
target_parent_type
not
in
parent_component_types
):
error
=
'You can not move {source_type} into {target_parent_type}.'
.
format
(
source_type
=
source_type
,
target_parent_type
=
target_parent_type
,
)
elif
source_parent
.
location
==
target_parent
.
location
:
error
=
'You can not move an item into the same parent.'
elif
source_item
.
location
==
target_parent
.
location
:
error
=
'You can not move an item into itself.'
elif
is_source_item_in_target_parents
(
source_item
,
target_parent
):
error
=
'You can not move an item into it
\'
s child.'
elif
source_index
is
None
:
error
=
'{source_usage_key} not found in {parent_usage_key}.'
.
format
(
source_usage_key
=
unicode
(
source_usage_key
),
...
...
@@ -1093,6 +1115,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
# a percent value out of 100, e.g. "58%" means "58/100".
pct_sign
=
_
(
'
%
'
))
user_partitions
=
get_user_partition_info
(
xblock
,
course
=
course
)
xblock_info
=
{
'id'
:
unicode
(
xblock
.
location
),
'display_name'
:
xblock
.
display_name_with_default
,
...
...
@@ -1101,6 +1124,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
if
is_concise
:
if
child_info
and
len
(
child_info
.
get
(
'children'
,
[]))
>
0
:
xblock_info
[
'child_info'
]
=
child_info
# Groups are labelled with their internal ids, rather than with the group name. Replace id with display name.
group_display_name
=
get_group_display_name
(
user_partitions
,
xblock_info
[
'display_name'
])
xblock_info
[
'display_name'
]
=
group_display_name
if
group_display_name
else
xblock_info
[
'display_name'
]
xblock_info
[
'has_children'
]
=
xblock
.
has_children
else
:
xblock_info
.
update
({
'edited_on'
:
get_default_time_display
(
xblock
.
subtree_edited_on
)
if
xblock
.
subtree_edited_on
else
None
,
...
...
@@ -1121,7 +1148,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'actions'
:
xblock_actions
,
'explanatory_message'
:
explanatory_message
,
'group_access'
:
xblock
.
group_access
,
'user_partitions'
:
get_user_partition_info
(
xblock
,
course
=
course
)
,
'user_partitions'
:
user_partitions
,
})
if
xblock
.
category
==
'sequential'
:
...
...
cms/djangoapps/contentstore/views/preview.py
View file @
de29ef94
...
...
@@ -274,6 +274,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'can_edit'
:
context
.
get
(
'can_edit'
,
True
),
'can_edit_visibility'
:
context
.
get
(
'can_edit_visibility'
,
True
),
'can_add'
:
context
.
get
(
'can_add'
,
True
),
'can_move'
:
context
.
get
(
'can_move'
,
True
)
}
html
=
render_to_string
(
'studio_xblock_wrapper.html'
,
template_context
)
frag
=
wrap_fragment
(
frag
,
html
)
...
...
cms/djangoapps/contentstore/views/tests/test_container_page.py
View file @
de29ef94
...
...
@@ -12,11 +12,13 @@ from django.utils import http
import
contentstore.views.component
as
views
from
contentstore.views.tests.utils
import
StudioPageTestCase
from
contentstore.tests.test_libraries
import
LibraryTestCase
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.factories
import
ItemFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
class
ContainerPageTestCase
(
StudioPageTestCase
):
class
ContainerPageTestCase
(
StudioPageTestCase
,
LibraryTestCase
):
"""
Unit tests for the container page.
"""
...
...
@@ -128,6 +130,44 @@ class ContainerPageTestCase(StudioPageTestCase):
self
.
validate_preview_html
(
published_child_container
,
self
.
container_view
)
self
.
validate_preview_html
(
published_child_vertical
,
self
.
reorderable_child_view
)
def
test_library_page_preview_html
(
self
):
"""
Verify that a library xblock's container (library page) preview returns the expected HTML.
"""
# Add some content to library.
self
.
_add_simple_content_block
()
self
.
validate_preview_html
(
self
.
library
,
self
.
container_view
,
can_reorder
=
False
,
can_move
=
False
)
def
test_library_content_preview_html
(
self
):
"""
Verify that a library content block container page preview returns the expected HTML.
"""
# Library content block is only supported in split courses.
with
modulestore
()
.
default_store
(
ModuleStoreEnum
.
Type
.
split
):
course
=
CourseFactory
.
create
()
# Add some content to library
self
.
_add_simple_content_block
()
# Create a library content block
lc_block
=
self
.
_add_library_content_block
(
course
,
self
.
lib_key
)
self
.
assertEqual
(
len
(
lc_block
.
children
),
0
)
# Refresh children to be reflected in lc_block
lc_block
=
self
.
_refresh_children
(
lc_block
)
self
.
assertEqual
(
len
(
lc_block
.
children
),
1
)
self
.
validate_preview_html
(
lc_block
,
self
.
container_view
,
can_add
=
False
,
can_reorder
=
False
,
can_move
=
False
,
can_edit
=
True
,
can_duplicate
=
False
,
can_delete
=
False
)
def
test_draft_container_preview_html
(
self
):
"""
Verify that a draft xblock's container preview returns the expected HTML.
...
...
cms/djangoapps/contentstore/views/tests/test_item.py
View file @
de29ef94
This diff is collapsed.
Click to expand it.
cms/djangoapps/contentstore/views/tests/utils.py
View file @
de29ef94
...
...
@@ -41,34 +41,48 @@ class StudioPageTestCase(CourseTestCase):
resp_content
=
json
.
loads
(
resp
.
content
)
return
resp_content
[
'html'
]
def
validate_preview_html
(
self
,
xblock
,
view_name
,
can_add
=
True
):
def
validate_preview_html
(
self
,
xblock
,
view_name
,
can_add
=
True
,
can_reorder
=
True
,
can_move
=
True
,
can_edit
=
True
,
can_duplicate
=
True
,
can_delete
=
True
):
"""
Verify that the specified xblock's preview has the expected HTML elements.
"""
html
=
self
.
get_preview_html
(
xblock
,
view_name
)
self
.
validate_html_for_add_buttons
(
html
,
can_add
)
# Verify drag handles always appear.
drag_handle_html
=
'<span data-tooltip="Drag to reorder" class="drag-handle action"></span>'
self
.
assertIn
(
drag_handle_html
,
html
)
# Verify that there are no action buttons for public blocks
expected_button_html
=
[
'<button class="btn-default edit-button action-button">'
,
self
.
validate_html_for_action_button
(
html
,
'<div class="add-xblock-component new-component-item adding"></div>'
,
can_add
)
self
.
validate_html_for_action_button
(
html
,
'<span data-tooltip="Drag to reorder" class="drag-handle action"></span>'
,
can_reorder
)
self
.
validate_html_for_action_button
(
html
,
'<button data-tooltip="Move" class="btn-default move-button action-button">'
,
can_move
)
self
.
validate_html_for_action_button
(
html
,
'button class="btn-default edit-button action-button">'
,
can_edit
)
self
.
validate_html_for_action_button
(
html
,
'<button data-tooltip="Delete" class="btn-default delete-button action-button">'
,
can_duplicate
)
self
.
validate_html_for_action_button
(
html
,
'<button data-tooltip="Duplicate" class="btn-default duplicate-button action-button">'
,
'<button data-tooltip="Move" class="btn-default move-button action-button">'
]
for
button_html
in
expected_button_html
:
self
.
assertIn
(
button_html
,
html
)
can_delete
)
def
validate_html_for_a
dd_buttons
(
self
,
html
,
can_add
=
True
):
def
validate_html_for_a
ction_button
(
self
,
html
,
expected_html
,
can_action
=
True
):
"""
Validate that the specified HTML has
the appropriate add actions for the current publish state
.
Validate that the specified HTML has
specific action.
.
"""
# Verify that there are no add buttons for public blocks
add_button_html
=
'<div class="add-xblock-component new-component-item adding"></div>'
if
can_add
:
self
.
assertIn
(
add_button_html
,
html
)
if
can_action
:
self
.
assertIn
(
expected_html
,
html
)
else
:
self
.
assertNotIn
(
add_button
_html
,
html
)
self
.
assertNotIn
(
expected
_html
,
html
)
cms/static/js/spec/views/move_xblock_spec.js
View file @
de29ef94
...
...
@@ -205,13 +205,17 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
return
category
+
'_display_name_'
+
xblockIndex
;
})
);
if
(
category
!==
'component'
)
{
if
(
category
===
'component'
)
{
if
(
hasCurrentLocation
)
{
expect
(
displayedInfo
.
currentLocationText
).
toEqual
(
'(Currently selected)'
);
}
}
else
{
if
(
hasCurrentLocation
)
{
expect
(
displayedInfo
.
currentLocationText
).
toEqual
(
'(Current location)'
);
}
expect
(
displayedInfo
.
forwardButtonSRTexts
).
toEqual
(
_
.
map
(
_
.
range
(
expectedXBlocksCount
),
function
()
{
return
'
Click for children
'
;
return
'
View child items
'
;
})
);
expect
(
displayedInfo
.
forwardButtonCount
).
toEqual
(
expectedXBlocksCount
);
...
...
@@ -519,15 +523,8 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
});
});
describe
(
'Move an xblock'
,
function
()
{
it
(
'can not move in a disabled state'
,
function
()
{
verifyMoveEnabled
(
false
);
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
click
();
expect
(
modal
.
movedAlertView
).
toBeNull
();
expect
(
getSentRequests
().
length
).
toEqual
(
0
);
});
it
(
'move button is disabled when navigating to same parent'
,
function
()
{
describe
(
'Move button'
,
function
()
{
it
(
'is disabled when navigating to same parent'
,
function
()
{
// select a target parent as the same as source parent and click
renderViews
(
courseOutline
);
_
.
each
(
_
.
range
(
3
),
function
()
{
...
...
@@ -536,7 +533,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
verifyMoveEnabled
(
'component'
,
true
);
});
it
(
'
move button
is enabled when navigating to different parent'
,
function
()
{
it
(
'is enabled when navigating to different parent'
,
function
()
{
// select a target parent as the different as source parent and click
renderViews
(
courseOutline
);
_
.
each
(
_
.
range
(
3
),
function
()
{
...
...
@@ -553,6 +550,111 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
verifyXBlockInfo
(
courseOutlineOptions
,
'section'
,
1
,
'forward'
,
false
);
});
it
(
'is disbabled when navigating to same source xblock'
,
function
()
{
var
outline
,
libraryContentXBlockInfo
=
{
category
:
'library_content'
,
display_name
:
'Library Content'
,
has_children
:
true
,
id
:
'LIBRARY_CONTENT_ID'
},
outlineOptions
=
{
library_content
:
1
,
component
:
1
};
// make above xblock source xblock.
modal
.
sourceXBlockInfo
=
libraryContentXBlockInfo
;
outline
=
createXBlockInfo
(
'component'
,
outlineOptions
,
libraryContentXBlockInfo
);
renderViews
(
outline
);
expect
(
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
hasClass
(
'is-disabled'
)).
toBeTruthy
();
// select a target parent
clickForwardButton
(
0
);
expect
(
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
hasClass
(
'is-disabled'
)).
toBeTruthy
();
});
it
(
'is disabled when navigating inside source content experiment'
,
function
()
{
var
outline
,
splitTestXBlockInfo
=
{
category
:
'split_test'
,
display_name
:
'Content Experiment'
,
has_children
:
true
,
id
:
'SPLIT_TEST_ID'
},
outlineOptions
=
{
split_test
:
1
,
unit
:
2
,
component
:
1
};
// make above xblock source xblock.
modal
.
sourceXBlockInfo
=
splitTestXBlockInfo
;
outline
=
createXBlockInfo
(
'unit'
,
outlineOptions
,
splitTestXBlockInfo
);
renderViews
(
outline
);
expect
(
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
hasClass
(
'is-disabled'
)).
toBeTruthy
();
// navigate to groups level
clickForwardButton
(
0
);
expect
(
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
hasClass
(
'is-disabled'
)).
toBeTruthy
();
// navigate to component level inside a group
clickForwardButton
(
0
);
// move should be disabled because we are navigating inside source xblock
expect
(
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
hasClass
(
'is-disabled'
)).
toBeTruthy
();
});
it
(
'is disabled when navigating to any content experiment groups'
,
function
()
{
var
outline
,
splitTestXBlockInfo
=
{
category
:
'split_test'
,
display_name
:
'Content Experiment'
,
has_children
:
true
,
id
:
'SPLIT_TEST_ID'
},
outlineOptions
=
{
split_test
:
1
,
unit
:
2
,
component
:
1
};
// group level should be disabled but component level inside groups should be movable
outline
=
createXBlockInfo
(
'unit'
,
outlineOptions
,
splitTestXBlockInfo
);
renderViews
(
outline
);
// move is disabled on groups level
expect
(
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
hasClass
(
'is-disabled'
)).
toBeTruthy
();
// navigate to component level inside a group
clickForwardButton
(
1
);
expect
(
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
hasClass
(
'is-disabled'
)).
toBeFalsy
();
});
it
(
'is enabled when navigating to any parentable component'
,
function
()
{
var
parentableXBlockInfo
=
{
category
:
'vertical'
,
display_name
:
'Parentable Component'
,
has_children
:
true
,
id
:
'PARENTABLE_ID'
};
renderViews
(
parentableXBlockInfo
);
// move is enabled on parentable xblocks.
expect
(
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
hasClass
(
'is-disabled'
)).
toBeFalsy
();
});
it
(
'is disabled when navigating to any non-parentable component'
,
function
()
{
var
nonParentableXBlockInfo
=
{
category
:
'html'
,
display_name
:
'Non Parentable Component'
,
has_children
:
false
,
id
:
'NON_PARENTABLE_ID'
};
renderViews
(
nonParentableXBlockInfo
);
// move is disabled on non-parent xblocks.
expect
(
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
hasClass
(
'is-disabled'
)).
toBeTruthy
();
});
});
describe
(
'Move an xblock'
,
function
()
{
it
(
'can not move in a disabled state'
,
function
()
{
verifyMoveEnabled
(
false
);
modal
.
$el
.
find
(
'.modal-actions .action-move'
).
click
();
expect
(
modal
.
movedAlertView
).
toBeNull
();
expect
(
getSentRequests
().
length
).
toEqual
(
0
);
});
it
(
'move an xblock when move button is clicked'
,
function
()
{
var
requests
=
AjaxHelpers
.
requests
(
this
);
moveXBlockWithSuccess
(
requests
);
...
...
cms/static/js/views/modals/move_xblock_modal.js
View file @
de29ef94
...
...
@@ -122,6 +122,7 @@ function($, Backbone, _, gettext, BaseView, XBlockViewUtils, MoveXBlockUtils, Ht
this
.
moveXBlockListView
=
new
MoveXBlockListView
(
{
model
:
new
XBlockInfoModel
(
courseOutlineInfo
,
{
parse
:
true
}),
sourceXBlockInfo
:
this
.
sourceXBlockInfo
,
ancestorInfo
:
ancestorInfo
}
);
...
...
@@ -136,12 +137,30 @@ function($, Backbone, _, gettext, BaseView, XBlockViewUtils, MoveXBlockUtils, Ht
}
},
isValidCategory
:
function
(
sourceParentType
,
targetParentType
,
targetHasChildren
)
{
var
basicBlockTypes
=
[
'course'
,
'chapter'
,
'sequential'
,
'vertical'
];
// Treat source parent component as vertical to support move child components under content experiment
// and other similar xblocks.
// eslint-disable-next-line no-param-reassign
sourceParentType
=
sourceParentType
===
'split_test'
?
'vertical'
:
sourceParentType
;
// Treat target parent component as a vertical to support move to parentable target parent components.
// Also, moving a component directly to content experiment is not allowed, we need to visit to group level.
if
(
targetHasChildren
&&
!
_
.
contains
(
basicBlockTypes
,
targetParentType
)
&&
targetParentType
!==
'split_test'
)
{
targetParentType
=
'vertical'
;
// eslint-disable-line no-param-reassign
}
return
targetParentType
===
sourceParentType
;
},
enableMoveOperation
:
function
(
targetParentXBlockInfo
)
{
var
isValidMove
=
false
,
sourceParentType
=
this
.
sourceParentXBlockInfo
.
get
(
'category'
),
targetParentType
=
targetParentXBlockInfo
.
get
(
'category'
);
targetParentType
=
targetParentXBlockInfo
.
get
(
'category'
),
targetHasChildren
=
targetParentXBlockInfo
.
get
(
'has_children'
);
if
(
targetParentType
===
sourceParentType
&&
this
.
sourceParentXBlockInfo
.
id
!==
targetParentXBlockInfo
.
id
)
{
if
(
this
.
isValidCategory
(
sourceParentType
,
targetParentType
,
targetHasChildren
)
&&
this
.
sourceParentXBlockInfo
.
id
!==
targetParentXBlockInfo
.
id
&&
// same parent case
this
.
sourceXBlockInfo
.
id
!==
targetParentXBlockInfo
.
id
)
{
// same source item case
isValidMove
=
true
;
this
.
targetParentXBlockInfo
=
targetParentXBlockInfo
;
}
...
...
cms/static/js/views/move_xblock_breadcrumb.js
View file @
de29ef94
...
...
@@ -13,10 +13,6 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, MoveXBlockBreadcrumbVi
var
MoveXBlockBreadcrumb
=
Backbone
.
View
.
extend
({
el
:
'.breadcrumb-container'
,
defaultRenderOptions
:
{
breadcrumbs
:
[
'Course Outline'
]
},
events
:
{
'click .parent-nav-button'
:
'handleBreadcrumbButtonPress'
},
...
...
@@ -29,7 +25,7 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, MoveXBlockBreadcrumbVi
render
:
function
(
options
)
{
HtmlUtils
.
setHtml
(
this
.
$el
,
this
.
template
(
_
.
extend
({},
this
.
defaultRenderOptions
,
options
)
)
this
.
template
(
options
)
);
Backbone
.
trigger
(
'move:breadcrumbRendered'
);
return
this
;
...
...
cms/static/js/views/move_xblock_list.js
View file @
de29ef94
...
...
@@ -33,7 +33,8 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
section
:
gettext
(
'Sections'
),
subsection
:
gettext
(
'Subsections'
),
unit
:
gettext
(
'Units'
),
component
:
gettext
(
'Components'
)
component
:
gettext
(
'Components'
),
group
:
gettext
(
'Groups'
)
},
events
:
{
...
...
@@ -43,6 +44,7 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
initialize
:
function
(
options
)
{
this
.
visitedAncestors
=
[];
this
.
template
=
HtmlUtils
.
template
(
MoveXBlockListViewTemplate
);
this
.
sourceXBlockInfo
=
options
.
sourceXBlockInfo
;
this
.
ancestorInfo
=
options
.
ancestorInfo
;
this
.
listenTo
(
Backbone
,
'move:breadcrumbButtonPressed'
,
this
.
handleBreadcrumbButtonPress
);
this
.
renderXBlockInfo
();
...
...
@@ -53,6 +55,7 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
this
.
$el
,
this
.
template
(
{
sourceXBlockId
:
this
.
sourceXBlockInfo
.
id
,
xblocks
:
this
.
childrenInfo
.
children
,
noChildText
:
this
.
getNoChildText
(),
categoryText
:
this
.
getCategoryText
(),
...
...
@@ -123,10 +126,14 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
* Set parent and child XBlock categories.
*/
setDisplayedXBlocksCategories
:
function
()
{
this
.
parentInfo
.
category
=
XBlockUtils
.
getXBlockType
(
this
.
parentInfo
.
parent
.
get
(
'category'
),
this
.
visitedAncestors
[
this
.
visitedAncestors
.
length
-
2
]
);
var
childCategory
=
'component'
;
this
.
parentInfo
.
category
=
XBlockUtils
.
getXBlockType
(
this
.
parentInfo
.
parent
.
get
(
'category'
));
if
(
!
_
.
contains
(
_
.
keys
(
this
.
categoryRelationMap
),
this
.
parentInfo
.
category
))
{
if
(
this
.
parentInfo
.
category
===
'split_test'
)
{
childCategory
=
'group'
;
// This is just to show groups text on group listing.
}
this
.
categoryRelationMap
[
this
.
parentInfo
.
category
]
=
childCategory
;
}
this
.
childrenInfo
.
category
=
this
.
categoryRelationMap
[
this
.
parentInfo
.
category
];
},
...
...
@@ -136,25 +143,19 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
* @returns {any} Integer or undefined
*/
getCurrentLocationIndex
:
function
()
{
var
category
,
ancestorXBlock
,
currentLocationIndex
;
if
(
this
.
childrenInfo
.
category
===
'component'
||
this
.
childrenInfo
.
children
.
length
===
0
)
{
return
currentLocationIndex
;
}
category
=
this
.
childrenInfo
.
children
[
0
].
get
(
'category'
);
ancestorXBlock
=
_
.
find
(
this
.
ancestorInfo
.
ancestors
,
function
(
ancestor
)
{
return
ancestor
.
category
===
category
;
}
);
if
(
ancestorXBlock
)
{
_
.
each
(
this
.
childrenInfo
.
children
,
function
(
xblock
,
index
)
{
if
(
ancestorXBlock
.
display_name
===
xblock
.
get
(
'display_name'
)
&&
ancestorXBlock
.
id
===
xblock
.
get
(
'id'
))
{
currentLocationIndex
=
index
;
}
});
}
var
self
=
this
,
currentLocationIndex
;
_
.
each
(
self
.
childrenInfo
.
children
,
function
(
xblock
,
index
)
{
if
(
xblock
.
get
(
'id'
)
===
self
.
sourceXBlockInfo
.
id
)
{
currentLocationIndex
=
index
;
}
else
{
_
.
each
(
self
.
ancestorInfo
.
ancestors
,
function
(
ancestor
)
{
if
(
ancestor
.
display_name
===
xblock
.
get
(
'display_name'
)
&&
ancestor
.
id
===
xblock
.
get
(
'id'
))
{
currentLocationIndex
=
index
;
}
});
}
});
return
currentLocationIndex
;
},
...
...
cms/static/sass/views/_container.scss
View file @
de29ef94
...
...
@@ -395,20 +395,21 @@
}
.component
{
display
:
block
;
display
:
inline-
block
;
color
:
$black
;
padding
:
(
$baseline
/
4
)
(
$baseline
/
2
);
}
.xblock-displayname
{
@include
float
(
left
);
}
.button-forward
,
.component
{
border
:
none
;
padding
:
(
$baseline
/
2
);
}
.button-forward
{
.xblock-displayname
{
@include
float
(
left
);
}
padding
:
(
$baseline
/
2
);
.forward-sr-icon
{
@include
float
(
right
);
...
...
cms/templates/js/move-xblock-list.underscore
View file @
de29ef94
...
...
@@ -13,16 +13,12 @@
<%- categoryText %>:
</span>
</div>
<ul class="xblock-items-container">
<ul class="xblock-items-container"
data-items-category="<%- XBlocksCategory %>"
>
<% for (var i = 0; i < xblocks.length; i++) {
var xblock = xblocks[i];
%>
<li class="xblock-item" data-item-index="<%- i %>">
<% if (XBlocksCategory === 'component') { %>
<span class="xblock-displayname component truncate">
<%- xblock.get('display_name') %>
</span>
<% } else { %>
<% if (sourceXBlockId !== xblock.id && (xblock.get('child_info') || XBlocksCategory !== 'component')) { %>
<button class="button-forward" >
<span class="xblock-displayname truncate">
<%- xblock.get('display_name') %>
...
...
@@ -33,8 +29,19 @@
</span>
<% } %>
<span class="icon fa fa-arrow-right forward-sr-icon" aria-hidden="true"></span>
<span class="sr forward-sr-text"><%- gettext("
Click for children
") %></span>
<span class="sr forward-sr-text"><%- gettext("
View child items
") %></span>
</button>
<% } else { %>
<span class="component">
<span class="xblock-displayname truncate">
<%- xblock.get('display_name') %>
</span>
<% if(currentLocationIndex === i) { %>
<span class="current-location">
(<%- gettext('Currently selected') %>)
</span>
<% } %>
</span>
<% } %>
</li>
<% } %>
...
...
cms/templates/studio_xblock_wrapper.html
View file @
de29ef94
...
...
@@ -89,7 +89,8 @@ messages = xblock.validate().to_json()
<span
class=
"sr"
>
${_("Duplicate")}
</span>
</button>
</li>
% endif
% if can_move:
<li
class=
"action-item action-move"
>
<button
data-tooltip=
"${_("
Move
")}"
class=
"btn-default move-button action-button"
>
<span
class=
"stack-move-icon fa-stack fa-lg "
>
...
...
common/lib/xmodule/xmodule/library_content_module.py
View file @
de29ef94
...
...
@@ -338,6 +338,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
'display_name'
:
self
.
display_name
or
self
.
url_name
,
}))
context
[
'can_edit_visibility'
]
=
False
context
[
'can_move'
]
=
False
self
.
render_children
(
context
,
fragment
,
can_reorder
=
False
,
can_add
=
False
)
# else: When shown on a unit page, don't show any sort of preview -
# just the status of this block in the validation area.
...
...
common/lib/xmodule/xmodule/library_root_xblock.py
View file @
de29ef94
...
...
@@ -80,6 +80,7 @@ class LibraryRoot(XBlock):
children_to_show
=
self
.
children
[
item_start
:
item_end
]
# pylint: disable=no-member
force_render
=
context
.
get
(
'force_render'
,
None
)
context
[
'can_move'
]
=
False
for
child_key
in
children_to_show
:
# Children must have a separate context from the library itself. Make a copy.
...
...
common/test/acceptance/pages/studio/container.py
View file @
de29ef94
...
...
@@ -63,11 +63,16 @@ class ContainerPage(PageObject, HelpMixin):
is_done
=
num_wrappers
==
(
num_initialized_xblocks
+
num_failed_xblocks
)
return
(
is_done
,
is_done
)
def
_loading_spinner_hidden
():
""" promise function to check loading spinner state """
is_spinner_hidden
=
self
.
q
(
css
=
'div.ui-loading.is-hidden'
)
.
present
return
is_spinner_hidden
,
is_spinner_hidden
# First make sure that an element with the view-container class is present on the page,
# and then wait for the loading spinner to go away and all the xblocks to be initialized.
return
(
self
.
q
(
css
=
'body.view-container'
)
.
present
and
self
.
q
(
css
=
'div.ui-loading.is-hidden'
)
.
present
and
Promise
(
_loading_spinner_hidden
,
'loading spinner is hidden.'
)
.
fulfill
()
and
Promise
(
_is_finished_loading
,
'Finished rendering the xblock wrappers.'
)
.
fulfill
()
)
...
...
@@ -102,6 +107,13 @@ class ContainerPage(PageObject, HelpMixin):
return
self
.
_get_xblocks
(
".is-active "
)
@property
def
displayed_children
(
self
):
"""
Return a list of displayed xblocks loaded on the container page.
"""
return
self
.
_get_xblocks
()[
0
]
.
children
@property
def
publish_title
(
self
):
"""
Returns the title as displayed on the publishing sidebar component.
...
...
@@ -262,6 +274,29 @@ class ContainerPage(PageObject, HelpMixin):
"""
return
_click_edit
(
self
,
'.edit-button'
,
'.xblock-studio_view'
)
def
verify_confirmation_message
(
self
,
message
):
"""
Verify for confirmation message.
"""
def
_verify_message
():
""" promise function to check confirmation message state """
text
=
self
.
q
(
css
=
'#page-alert .alert.confirmation #alert-confirmation-title'
)
.
text
return
text
and
message
in
text
[
0
]
self
.
wait_for
(
_verify_message
,
description
=
'confirmation message present'
)
def
click_undo_move_link
(
self
):
"""
Click undo move link.
"""
click_css
(
self
,
'#page-alert .alert.confirmation .nav-actions .action-primary'
)
def
click_take_me_there_link
(
self
):
"""
Click take me there link.
"""
click_css
(
self
,
'#page-alert .alert.confirmation .nav-actions .action-secondary'
,
require_notification
=
False
)
def
add_missing_groups
(
self
):
"""
Click the "add missing groups" link.
...
...
@@ -382,7 +417,7 @@ class XBlockWrapper(PageObject):
"""
Will return any first-generation descendant xblocks of this xblock.
"""
descendants
=
self
.
q
(
css
=
self
.
_bounded_selector
(
self
.
BODY_SELECTOR
))
.
map
(
descendants
=
self
.
q
(
css
=
self
.
_bounded_selector
(
self
.
BODY_SELECTOR
))
.
filter
(
lambda
el
:
el
.
is_displayed
())
.
map
(
lambda
el
:
XBlockWrapper
(
self
.
browser
,
el
.
get_attribute
(
'data-locator'
)))
.
results
# Now remove any non-direct descendants.
...
...
@@ -468,6 +503,13 @@ class XBlockWrapper(PageObject):
"""
return
self
.
q
(
css
=
self
.
_bounded_selector
(
'.visibility-button'
))
.
is_present
()
@property
def
has_move_modal_button
(
self
):
"""
Returns True if this xblock has move modal button else False
"""
return
self
.
q
(
css
=
self
.
_bounded_selector
(
'.move-button'
))
.
is_present
()
def
go_to_container
(
self
):
"""
Open the container page linked to by this xblock, and return
...
...
@@ -505,6 +547,15 @@ class XBlockWrapper(PageObject):
"""
self
.
_click_button
(
'settings_tab'
)
def
open_move_modal
(
self
):
"""
Opens the move modal.
"""
click_css
(
self
,
'.move-button'
,
require_notification
=
False
)
self
.
wait_for
(
lambda
:
self
.
q
(
css
=
'.modal-window.move-modal'
)
.
visible
,
description
=
'move modal is visible'
)
def
set_field_val
(
self
,
field_display_name
,
field_value
):
"""
If editing, set the value of a field.
...
...
common/test/acceptance/pages/studio/move_xblock.py
0 → 100644
View file @
de29ef94
"""
Move XBlock Modal Page Object
"""
from
bok_choy.page_object
import
PageObject
from
common.test.acceptance.pages.common.utils
import
click_css
class
MoveModalView
(
PageObject
):
"""
A base class for move xblock
"""
def
__init__
(
self
,
browser
):
"""
Arguments:
browser (selenium.webdriver): The Selenium-controlled browser that this page is loaded in.
"""
super
(
MoveModalView
,
self
)
.
__init__
(
browser
)
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
'.modal-window.move-modal'
)
.
present
def
url
(
self
):
"""
Returns None because this is not directly accessible via URL.
"""
return
None
def
save
(
self
):
"""
Clicks save button.
"""
click_css
(
self
,
'a.action-save'
)
def
cancel
(
self
):
"""
Clicks cancel button.
"""
click_css
(
self
,
'a.action-cancel'
,
require_notification
=
False
)
def
click_forward_button
(
self
,
source_index
):
"""
Click forward button at specified `source_index`.
"""
css
=
'.move-modal .xblock-items-container .xblock-item'
self
.
q
(
css
=
'.button-forward'
)
.
nth
(
source_index
)
.
click
()
self
.
wait_for
(
lambda
:
len
(
self
.
q
(
css
=
css
)
.
results
)
>
0
,
description
=
'children are visible'
)
def
click_move_button
(
self
):
"""
Click move button.
"""
self
.
q
(
css
=
'.modal-actions .action-move'
)
.
first
.
click
()
@property
def
is_move_button_enabled
(
self
):
"""
Returns True if move button on modal is enabled else False.
"""
return
not
self
.
q
(
css
=
'.modal-actions .action-move.is-disabled'
)
.
present
@property
def
children_category
(
self
):
"""
Get displayed children category.
"""
return
self
.
q
(
css
=
'.xblock-items-container'
)
.
attrs
(
'data-items-category'
)[
0
]
def
navigate_to_category
(
self
,
category
,
navigation_options
):
"""
Navigates to specifec `category` for a specified `source_index`.
"""
child_category
=
self
.
children_category
while
child_category
!=
category
:
self
.
click_forward_button
(
navigation_options
[
child_category
])
child_category
=
self
.
children_category
common/test/acceptance/tests/studio/test_studio_container.py
View file @
de29ef94
...
...
@@ -10,6 +10,7 @@ from common.test.acceptance.fixtures.course import XBlockFixtureDesc
from
common.test.acceptance.pages.studio.component_editor
import
ComponentEditorView
,
ComponentVisibilityEditorView
from
common.test.acceptance.pages.studio.container
import
ContainerPage
from
common.test.acceptance.pages.studio.html_component_editor
import
HtmlComponentEditorView
from
common.test.acceptance.pages.studio.move_xblock
import
MoveModalView
from
common.test.acceptance.pages.studio.utils
import
add_discussion
,
drag
from
common.test.acceptance.pages.lms.courseware
import
CoursewarePage
from
common.test.acceptance.pages.lms.staff_view
import
StaffPage
...
...
@@ -1136,3 +1137,231 @@ class ProblemCategoryTabsTest(ContainerBase):
"Text Input with Hints and Feedback"
,
]
self
.
assertEqual
(
page
.
get_category_tab_components
(
'problem'
,
1
),
expected_components
)
@attr
(
shard
=
1
)
class
MoveComponentTest
(
ContainerBase
):
"""
Tests of moving an XBlock to another XBlock.
"""
def
setUp
(
self
,
is_staff
=
True
):
super
(
MoveComponentTest
,
self
)
.
setUp
(
is_staff
=
is_staff
)
self
.
container
=
ContainerPage
(
self
.
browser
,
None
)
self
.
move_modal_view
=
MoveModalView
(
self
.
browser
)
self
.
navigation_options
=
{
'section'
:
0
,
'subsection'
:
0
,
'unit'
:
1
,
}
self
.
source_component_display_name
=
'HTML 11'
self
.
source_xblock_category
=
'component'
self
.
message_move
=
'Success! "{display_name}" has been moved.'
self
.
message_undo
=
'Move cancelled. "{display_name}" has been moved back to its original location.'
def
populate_course_fixture
(
self
,
course_fixture
):
"""
Sets up a course structure.
"""
# pylint: disable=attribute-defined-outside-init
self
.
unit_page1
=
XBlockFixtureDesc
(
'vertical'
,
'Test Unit 1'
)
.
add_children
(
XBlockFixtureDesc
(
'html'
,
'HTML 11'
),
XBlockFixtureDesc
(
'html'
,
'HTML 12'
)
)
self
.
unit_page2
=
XBlockFixtureDesc
(
'vertical'
,
'Test Unit 2'
)
.
add_children
(
XBlockFixtureDesc
(
'html'
,
'HTML 21'
),
XBlockFixtureDesc
(
'html'
,
'HTML 22'
)
)
course_fixture
.
add_children
(
XBlockFixtureDesc
(
'chapter'
,
'Test Section'
)
.
add_children
(
XBlockFixtureDesc
(
'sequential'
,
'Test Subsection'
)
.
add_children
(
self
.
unit_page1
,
self
.
unit_page2
)
)
)
def
verify_move_opertions
(
self
,
unit_page
,
source_component
,
operation
,
component_display_names_after_operation
):
"""
Verify move operations.
Arguments:
unit_page (Object) Unit container page.
source_component (Object) source XBlock object to be moved.
operation (str), `move` or `undo move` operation.
component_display_names_after_operation (dict) display names of components after operation in source/dest
"""
source_component
.
open_move_modal
()
self
.
move_modal_view
.
navigate_to_category
(
self
.
source_xblock_category
,
self
.
navigation_options
)
self
.
assertEqual
(
self
.
move_modal_view
.
is_move_button_enabled
,
True
)
self
.
move_modal_view
.
click_move_button
()
self
.
container
.
verify_confirmation_message
(
self
.
message_move
.
format
(
display_name
=
self
.
source_component_display_name
)
)
self
.
assertEqual
(
len
(
unit_page
.
displayed_children
),
1
)
if
operation
==
'move'
:
self
.
container
.
click_take_me_there_link
()
elif
operation
==
'undo_move'
:
self
.
container
.
click_undo_move_link
()
self
.
container
.
verify_confirmation_message
(
self
.
message_undo
.
format
(
display_name
=
self
.
source_component_display_name
)
)
unit_page
=
ContainerPage
(
self
.
browser
,
None
)
components
=
unit_page
.
displayed_children
self
.
assertEqual
(
[
component
.
name
for
component
in
components
],
component_display_names_after_operation
)
def
test_move_component_successfully
(
self
):
"""
Test if we can move a component successfully.
Given I am a staff user
And I go to unit page in first section
And I open the move modal
And I navigate to unit in second section
And I see move button is enabled
When I click on the move button
Then I see move operation success message
And When I click on take me there link
Then I see moved component there.
"""
unit_page
=
self
.
go_to_unit_page
(
unit_name
=
'Test Unit 1'
)
components
=
unit_page
.
displayed_children
self
.
assertEqual
(
len
(
components
),
2
)
self
.
verify_move_opertions
(
unit_page
=
unit_page
,
source_component
=
components
[
0
],
operation
=
'move'
,
component_display_names_after_operation
=
[
'HTML 21'
,
'HTML 22'
,
'HTML 11'
]
)
def
test_undo_move_component_successfully
(
self
):
"""
Test if we can undo move a component successfully.
Given I am a staff user
And I go to unit page in first section
And I open the move modal
When I click on the move button
Then I see move operation successful message
And When I clicked on undo move link
Then I see that undo move operation is successful
"""
unit_page
=
self
.
go_to_unit_page
(
unit_name
=
'Test Unit 1'
)
components
=
unit_page
.
displayed_children
self
.
assertEqual
(
len
(
components
),
2
)
self
.
verify_move_opertions
(
unit_page
=
unit_page
,
source_component
=
components
[
0
],
operation
=
'undo_move'
,
component_display_names_after_operation
=
[
'HTML 11'
,
'HTML 12'
]
)
def
test_content_experiment
(
self
):
"""
Test if we can move a component of content experiment successfully.
Given that I am a staff user
And I go to content experiment page
And I open the move dialogue modal
When I navigate to the unit in second section
Then I see move button is enabled
And when I click on the move button
Then I see move operation success message
And when I click on take me there link
Then I see moved component there
And when I undo move a component
Then I see that undo move operation success message
"""
# Add content experiment support to course.
self
.
course_fixture
.
add_advanced_settings
({
u'advanced_modules'
:
{
'value'
:
[
'split_test'
]},
})
# Create group configurations
# pylint: disable=protected-access
self
.
course_fixture
.
_update_xblock
(
self
.
course_fixture
.
_course_location
,
{
'metadata'
:
{
u'user_partitions'
:
[
create_user_partition_json
(
0
,
'Test Group Configuration'
,
'Description of the group configuration.'
,
[
Group
(
'0'
,
'Group A'
),
Group
(
'1'
,
'Group B'
)]
),
],
},
})
# Add split test to unit_page1 and assign newly created group configuration to it
split_test
=
XBlockFixtureDesc
(
'split_test'
,
'Test Content Experiment'
,
metadata
=
{
'user_partition_id'
:
0
})
self
.
course_fixture
.
create_xblock
(
self
.
unit_page1
.
locator
,
split_test
)
# Visit content experiment container page.
unit_page
=
ContainerPage
(
self
.
browser
,
split_test
.
locator
)
unit_page
.
visit
()
group_a_locator
=
unit_page
.
displayed_children
[
0
]
.
locator
# Add some components to Group A.
self
.
course_fixture
.
create_xblock
(
group_a_locator
,
XBlockFixtureDesc
(
'html'
,
'HTML 311'
)
)
self
.
course_fixture
.
create_xblock
(
group_a_locator
,
XBlockFixtureDesc
(
'html'
,
'HTML 312'
)
)
# Go to group page to move it's component.
group_container_page
=
ContainerPage
(
self
.
browser
,
group_a_locator
)
group_container_page
.
visit
()
# Verify content experiment block has correct groups and components.
components
=
group_container_page
.
displayed_children
self
.
assertEqual
(
len
(
components
),
2
)
self
.
source_component_display_name
=
'HTML 311'
# Verify undo move operation for content experiment.
self
.
verify_move_opertions
(
unit_page
=
group_container_page
,
source_component
=
components
[
0
],
operation
=
'undo_move'
,
component_display_names_after_operation
=
[
'HTML 311'
,
'HTML 312'
]
)
# Verify move operation for content experiment.
self
.
verify_move_opertions
(
unit_page
=
group_container_page
,
source_component
=
components
[
0
],
operation
=
'move'
,
component_display_names_after_operation
=
[
'HTML 21'
,
'HTML 22'
,
'HTML 311'
]
)
def
test_a11y
(
self
):
"""
Verify move modal a11y.
"""
unit_page
=
self
.
go_to_unit_page
(
unit_name
=
'Test Unit 1'
)
unit_page
.
a11y_audit
.
config
.
set_scope
(
include
=
[
".modal-window.move-modal"
]
)
unit_page
.
a11y_audit
.
config
.
set_rules
({
'ignore'
:
[
'color-contrast'
,
# TODO: AC-716
'link-href'
,
# TODO: AC-716
]
})
unit_page
.
displayed_children
[
0
]
.
open_move_modal
()
for
category
in
[
'section'
,
'subsection'
,
'component'
]:
self
.
move_modal_view
.
navigate_to_category
(
category
,
self
.
navigation_options
)
unit_page
.
a11y_audit
.
check_for_accessibility_errors
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment