feat: updated pages to the new custom pages design (#28686)
made changes to pages template refactored method to handle reordering of static tabs refactored test for the refactored method added link to the pages and resources MFE on the updated page
This commit is contained in:
@@ -42,11 +42,15 @@ class TabsAPITests(CourseTestCase):
|
||||
)
|
||||
|
||||
# add a static tab to the course, for code coverage
|
||||
self.test_tab = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="static_tab",
|
||||
display_name="Static_1",
|
||||
)
|
||||
# add 4 static tabs to the course, for code coverage
|
||||
self.test_tabs = []
|
||||
for i in range(1, 5):
|
||||
tab = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="static_tab",
|
||||
display_name=f"Static_{i}"
|
||||
)
|
||||
self.test_tabs.append(tab)
|
||||
self.reload_course()
|
||||
|
||||
def check_invalid_response(self, resp):
|
||||
@@ -107,13 +111,13 @@ class TabsAPITests(CourseTestCase):
|
||||
# reorder the last two tabs
|
||||
tab_ids[num_orig_tabs - 1], tab_ids[num_orig_tabs - 2] = tab_ids[num_orig_tabs - 2], tab_ids[num_orig_tabs - 1]
|
||||
|
||||
# remove the middle tab
|
||||
# remove the third to the last tab, the removed tab has to be a static tab
|
||||
# (the code needs to handle the case where tabs requested for re-ordering is a subset of the tabs in the course)
|
||||
removed_tab = tab_ids.pop(num_orig_tabs // 2)
|
||||
assert len(tab_ids) == num_orig_tabs - 1
|
||||
removed_tab = tab_ids.pop(num_orig_tabs - 3)
|
||||
self.assertEqual(len(tab_ids), num_orig_tabs - 1)
|
||||
|
||||
# post the request
|
||||
resp = self.make_reorder_tabs_request([{"tab_id": tab_id} for tab_id in tab_ids])
|
||||
resp = self.make_reorder_tabs_request([{"tab_id": tab_id} for tab_id in tab_ids if 'static' in tab_id])
|
||||
assert resp.status_code == 204
|
||||
|
||||
# reload the course and verify the new tab order
|
||||
|
||||
@@ -13,7 +13,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
|
||||
from ..serializers import CourseTabSerializer, CourseTabUpdateSerializer, TabIDLocatorSerializer
|
||||
from ....views.tabs import edit_tab_handler, get_course_tabs, reorder_tabs_handler
|
||||
from ....views.tabs import edit_tab_handler, get_course_static_tabs, reorder_tabs_handler
|
||||
|
||||
|
||||
@view_auth_classes(is_authenticated=True)
|
||||
@@ -34,7 +34,7 @@ class CourseTabListView(DeveloperErrorViewMixin, APIView):
|
||||
@verify_course_exists()
|
||||
def get(self, request: Request, course_id: str) -> Response:
|
||||
"""
|
||||
Get a list of all the tabs in a course including hidden tabs.
|
||||
Get a list of all the static tabs in a course including hidden tabs.
|
||||
|
||||
**Example Request**
|
||||
|
||||
@@ -82,7 +82,7 @@ class CourseTabListView(DeveloperErrorViewMixin, APIView):
|
||||
self.permission_denied(request)
|
||||
|
||||
course_module = modulestore().get_course(course_key)
|
||||
tabs_to_render = get_course_tabs(course_module, request.user)
|
||||
tabs_to_render = get_course_static_tabs(course_module, request.user)
|
||||
return Response(CourseTabSerializer(tabs_to_render, many=True).data)
|
||||
|
||||
|
||||
|
||||
@@ -61,10 +61,10 @@ def tabs_handler(request, course_key_string):
|
||||
return JsonResponse()
|
||||
|
||||
elif request.method == "GET": # assume html
|
||||
# get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs
|
||||
# get all tabs from the tabs list and select only static tabs (a.k.a. user-created tabs)
|
||||
# present in the same order they are displayed in LMS
|
||||
|
||||
tabs_to_render = list(get_course_tabs(course_item, request.user))
|
||||
tabs_to_render = list(get_course_static_tabs(course_item, request.user))
|
||||
|
||||
return render_to_response(
|
||||
"edit-tabs.html",
|
||||
@@ -78,9 +78,9 @@ def tabs_handler(request, course_key_string):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
def get_course_tabs(course_item: CourseBlock, user: User) -> Iterable[CourseTab]:
|
||||
def get_course_static_tabs(course_item: CourseBlock, user: User) -> Iterable[CourseTab]:
|
||||
"""
|
||||
Yields all the course tabs in a course including hidden tabs.
|
||||
Yields all the static tabs in a course including hidden tabs.
|
||||
|
||||
Args:
|
||||
course_item (CourseBlock): The course object from which to get the tabs
|
||||
@@ -96,7 +96,7 @@ def get_course_tabs(course_item: CourseBlock, user: User) -> Iterable[CourseTab]
|
||||
# static tab needs its locator information to render itself as an xmodule
|
||||
static_tab_loc = course_item.id.make_usage_key("static_tab", tab.url_slug)
|
||||
tab.locator = static_tab_loc
|
||||
yield tab
|
||||
yield tab
|
||||
|
||||
|
||||
def update_tabs_handler(course_item: CourseBlock, tabs_data: Dict, user: User) -> None:
|
||||
@@ -119,7 +119,7 @@ def update_tabs_handler(course_item: CourseBlock, tabs_data: Dict, user: User) -
|
||||
|
||||
def reorder_tabs_handler(course_item, tabs_data, user):
|
||||
"""
|
||||
Helper function for handling reorder of tabs request
|
||||
Helper function for handling reorder of static tabs request
|
||||
"""
|
||||
|
||||
# Tabs are identified by tab_id or locators.
|
||||
@@ -128,10 +128,40 @@ def reorder_tabs_handler(course_item, tabs_data, user):
|
||||
# their tab_ids since the xmodule editor uses only locators to identify new objects.
|
||||
requested_tab_id_locators = tabs_data["tabs"]
|
||||
|
||||
# original tab list in original order
|
||||
old_tab_list = course_item.tabs
|
||||
#get original tab list of only static tabs with their original index(position) in the full course tabs list
|
||||
old_tab_dict = {}
|
||||
for idx, tab in enumerate(course_item.tabs):
|
||||
if isinstance(tab, StaticTab):
|
||||
old_tab_dict[tab] = idx
|
||||
old_tab_list = list(old_tab_dict.keys())
|
||||
|
||||
# create a new list in the new order
|
||||
new_tab_list = create_new_list(requested_tab_id_locators, old_tab_list)
|
||||
|
||||
# Creates a full new course tab list of both default and static course tabs
|
||||
# by looping through the new tab list of static only tabs and
|
||||
# putting them in their new position in the list of course item tabs
|
||||
# original_idx gives the list of positions of all static tabs in course tabs originally
|
||||
full_new_tab_list = course_item.tabs
|
||||
original_idx = list(old_tab_dict.values())
|
||||
for i in range(len(new_tab_list)):
|
||||
full_new_tab_list[original_idx[i]] = new_tab_list[i]
|
||||
|
||||
# validate the tabs to make sure everything is Ok (e.g., did the client try to reorder unmovable tabs?)
|
||||
try:
|
||||
CourseTabList.validate_tabs(full_new_tab_list)
|
||||
except InvalidTabsException as exception:
|
||||
raise ValidationError({"error": f"New list of tabs is not valid: {str(exception)}."}) from exception
|
||||
|
||||
# persist the new order of the tabs
|
||||
course_item.tabs = full_new_tab_list
|
||||
modulestore().update_item(course_item, user.id)
|
||||
|
||||
|
||||
def create_new_list(requested_tab_id_locators, old_tab_list):
|
||||
"""
|
||||
Helper function for creating a new course tab list in the new order
|
||||
of reordered tabs
|
||||
"""
|
||||
new_tab_list = []
|
||||
for tab_id_locator in requested_tab_id_locators:
|
||||
tab = get_tab_by_tab_id_locator(old_tab_list, tab_id_locator)
|
||||
@@ -143,16 +173,7 @@ def reorder_tabs_handler(course_item, tabs_data, user):
|
||||
# global or course settings. so add those to the end of the list.
|
||||
non_displayed_tabs = set(old_tab_list) - set(new_tab_list)
|
||||
new_tab_list.extend(non_displayed_tabs)
|
||||
|
||||
# validate the tabs to make sure everything is Ok (e.g., did the client try to reorder unmovable tabs?)
|
||||
try:
|
||||
CourseTabList.validate_tabs(new_tab_list)
|
||||
except InvalidTabsException as exception:
|
||||
raise ValidationError({"error": f"New list of tabs is not valid: {str(exception)}."}) from exception
|
||||
|
||||
# persist the new order of the tabs
|
||||
course_item.tabs = new_tab_list
|
||||
modulestore().update_item(course_item, user.id)
|
||||
return new_tab_list
|
||||
|
||||
|
||||
def edit_tab_handler(course_item: CourseBlock, tabs_data: Dict, user: User):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
""" Tests for tab functions (just primitive). """
|
||||
|
||||
|
||||
import json
|
||||
|
||||
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
|
||||
@@ -25,12 +24,15 @@ class TabsPageTests(CourseTestCase):
|
||||
# Set the URL for tests
|
||||
self.url = reverse_course_url('tabs_handler', self.course.id)
|
||||
|
||||
# add a static tab to the course, for code coverage
|
||||
self.test_tab = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="static_tab",
|
||||
display_name="Static_1"
|
||||
)
|
||||
# add 4 static tabs to the course, for code coverage
|
||||
self.test_tabs = []
|
||||
for i in range(1, 5):
|
||||
tab = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="static_tab",
|
||||
display_name=f"Static_{i}"
|
||||
)
|
||||
self.test_tabs.append(tab)
|
||||
self.reload_course()
|
||||
|
||||
def check_invalid_tab_id_response(self, resp):
|
||||
@@ -39,7 +41,6 @@ class TabsPageTests(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
resp_content = json.loads(resp.content.decode('utf-8'))
|
||||
self.assertIn("error", resp_content)
|
||||
self.assertIn("invalid_tab_id", resp_content['error'])
|
||||
|
||||
def test_not_implemented(self):
|
||||
"""Verify not implemented errors"""
|
||||
@@ -85,15 +86,15 @@ class TabsPageTests(CourseTestCase):
|
||||
# reorder the last two tabs
|
||||
tab_ids[num_orig_tabs - 1], tab_ids[num_orig_tabs - 2] = tab_ids[num_orig_tabs - 2], tab_ids[num_orig_tabs - 1]
|
||||
|
||||
# remove the middle tab
|
||||
# remove the third to the last tab, the removed tab has to be a static tab
|
||||
# (the code needs to handle the case where tabs requested for re-ordering is a subset of the tabs in the course)
|
||||
removed_tab = tab_ids.pop(num_orig_tabs // 2)
|
||||
removed_tab = tab_ids.pop(num_orig_tabs - 3)
|
||||
self.assertEqual(len(tab_ids), num_orig_tabs - 1)
|
||||
|
||||
# post the request
|
||||
# post the request with the reordered static tabs only
|
||||
resp = self.client.ajax_post(
|
||||
self.url,
|
||||
data={'tabs': [{'tab_id': tab_id} for tab_id in tab_ids]},
|
||||
data={'tabs': [{'tab_id': tab_id} for tab_id in tab_ids if 'static' in tab_id]},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
@@ -178,7 +179,7 @@ class TabsPageTests(CourseTestCase):
|
||||
"""
|
||||
Verify that the static tab renders itself with the correct HTML
|
||||
"""
|
||||
preview_url = f'/xblock/{self.test_tab.location}/{STUDENT_VIEW}'
|
||||
preview_url = f'/xblock/{self.test_tabs[0].location}/{STUDENT_VIEW}'
|
||||
|
||||
resp = self.client.get(preview_url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
BIN
cms/static/images/preview-lms-staticpage.png
Normal file
BIN
cms/static/images/preview-lms-staticpage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -7,6 +7,7 @@
|
||||
from django.urls import reverse
|
||||
from xmodule.tabs import StaticTab
|
||||
from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
from cms.djangoapps.contentstore.utils import get_pages_and_resources_url
|
||||
%>
|
||||
<%block name="title">${_("Pages")}</%block>
|
||||
<%block name="bodyclass">is-signedin course view-static-pages</%block>
|
||||
@@ -27,12 +28,40 @@
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
% if context_course:
|
||||
<%
|
||||
pages_and_resources_mfe_url = get_pages_and_resources_url(context_course.id)
|
||||
pages_and_resources_mfe_enabled = bool(pages_and_resources_mfe_url)
|
||||
%>
|
||||
% endif
|
||||
|
||||
% if pages_and_resources_mfe_enabled:
|
||||
<header class="mast has-actions">
|
||||
<div class="jump-nav">
|
||||
<nav class="nav-dd title ui-left">
|
||||
<ol>
|
||||
<li class="nav-item">
|
||||
<span class="label">${_("Content")}</span>
|
||||
<span class="spacer"> ›</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="title" href="${pages_and_resources_mfe_url}" rel="external">${_("Pages & Resources")}</a>
|
||||
<span class="spacer"> ›</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<h1 class="page-header">
|
||||
<span class="sr">> </span>${_("Custom Pages")}
|
||||
</h1>
|
||||
% else:
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<h1 class="page-header">
|
||||
<small class="subtitle">${_("Content")}</small>
|
||||
## Translators: Pages refer to the tabs that appear in the top navigation of each course.
|
||||
<span class="sr"> > </span>${_("Pages")}
|
||||
## Translators: Custom Pages refer to the tabs that appear in the top navigation of each course.
|
||||
<span class="sr"> > </span>${_("Custom Pages")}
|
||||
</h1>
|
||||
% endif
|
||||
|
||||
<nav class="nav-actions" aria-label="${_('Page Actions')}">
|
||||
<h3 class="sr">${_("Page Actions")}</h3>
|
||||
@@ -72,59 +101,8 @@
|
||||
css_class = css_class + " is-fixed"
|
||||
%>
|
||||
|
||||
% if isinstance(tab, StaticTab):
|
||||
<li class="component ${css_class}" data-locator="${tab.locator}" data-tab-id="${tab.tab_id}"></li>
|
||||
<li class="component ${css_class}" data-locator="${tab.locator}" data-tab-id="${tab.tab_id}"></li>
|
||||
|
||||
% else:
|
||||
<li class="course-nav-item ${css_class}" data-tab-id="${tab.tab_id}">
|
||||
<div class="course-nav-item-header">
|
||||
|
||||
% if tab.is_collection:
|
||||
|
||||
<h3 class="title-sub">${_(tab.name)}</h3>
|
||||
<ul class="course-nav-item-children">
|
||||
% for item in tab.items(context_course):
|
||||
<li class="course-nav-item-child title">
|
||||
${_(item.name)}
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
% else:
|
||||
<h3 class="title">${_(tab.name)}</h3>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<div class="course-nav-item-actions wrapper-actions-list">
|
||||
<ul class="actions-list">
|
||||
|
||||
% if tab.is_hideable:
|
||||
<li class="action-item action-visible">
|
||||
<label><span class="sr">${_("Show this page")}</span></label>
|
||||
% if tab.is_hidden:
|
||||
<input type="checkbox" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" checked />
|
||||
% else:
|
||||
<input type="checkbox" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" />
|
||||
% endif
|
||||
<div class="action-button"><span class="icon fa fa-eye" aria-hidden="true"></span><span class="icon fa fa-eye-slash"></span></div>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
% if tab.is_movable:
|
||||
<div class="drag-handle action" data-tooltip="${_('Drag to reorder')}">
|
||||
<span class="sr">${_("Drag to reorder")}</span>
|
||||
</div>
|
||||
% else:
|
||||
<div class="drag-handle is-fixed" data-tooltip="${_('This page cannot be reordered')}">
|
||||
<span class="sr">${_("This page cannot be reordered")}</span>
|
||||
</div>
|
||||
% endif
|
||||
</li>
|
||||
|
||||
% endif
|
||||
% endfor
|
||||
|
||||
<li class="new-component-item"></li>
|
||||
@@ -140,16 +118,12 @@
|
||||
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What are pages?")}</h3>
|
||||
<p>${_("Pages are listed horizontally at the top of your course. Default pages (Home, Course, Discussion, Wiki, and Progress) are followed by textbooks and custom pages that you create.")}</p>
|
||||
<h3 class="title-3">${_("What are custom pages?")}</h3>
|
||||
<p>${_("You can create and edit custom pages to provide students with additional course content. For example, you can create pages for the grading policy, course syllabus, and a course calendar. ")} </p>
|
||||
</div>
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Custom pages")}</h3>
|
||||
<p>${_("You can create and edit custom pages to provide students with additional course content. For example, you can create pages for the grading policy, course slides, and a course calendar. ")} </p>
|
||||
</div>
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("How do pages look to students in my course?")}</h3>
|
||||
<p>${_("Students see the default and custom pages at the top of your course and use these links to navigate.")} <br /> <a rel="modal" href="#preview-lms-staticpages">${_("See an example")}</a></p>
|
||||
<h3 class="title-3">${_("How do custom pages look to students in my course?")}</h3>
|
||||
<p>${_("Custom pages are listed horizontally at the top of your course after default pages.")} <br /> <a rel="modal" href="#preview-lms-staticpages">${_("See an example")}</a></p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -157,10 +131,10 @@
|
||||
</div>
|
||||
|
||||
<div class="content-modal" id="preview-lms-staticpages">
|
||||
<h3 class="title">${_("Pages in Your Course")}</h3>
|
||||
<h3 class="title">${_("Custom pages in your course")}</h3>
|
||||
<figure>
|
||||
<img src="${static.url("images/preview-lms-staticpages.png")}" alt="${_('Preview of Pages in your course')}" />
|
||||
<figcaption class="description">${_("Pages appear in your course's top navigation bar. The default pages (Home, Course, Discussion, Wiki, and Progress) are followed by textbooks and custom pages.")}</figcaption>
|
||||
<img src="${static.url("images/preview-lms-staticpage.png")}" alt="${_('Preview of Pages in your course')}" />
|
||||
<figcaption class="description">${_("Custom pages are listed horizontally at the top of your course after default pages and textbooks. In the above example, custom pages for \"Course Schedule\" and \"Supplements\" have been added to a course.")}</figcaption>
|
||||
</figure>
|
||||
|
||||
<a href="#" rel="view" class="action action-modal-close">
|
||||
|
||||
Reference in New Issue
Block a user