From 2a7380dec7d4f61eb22d40f1ad07d6aad55b8e50 Mon Sep 17 00:00:00 2001 From: Alejandro Cardenas Date: Tue, 17 Nov 2020 14:07:46 -0500 Subject: [PATCH 1/9] feat: adding username patterns allowed Username pattern added to validate username in registration form. --- cms/envs/common.py | 2 +- cms/envs/production.py | 1 + lms/envs/common.py | 1 + lms/envs/production.py | 1 + .../core/djangoapps/user_authn/views/registration_form.py | 8 ++++++++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index bfecbffcaad..4a19fe758c8 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -52,7 +52,7 @@ # https://open-edx-proposals.readthedocs.io/en/latest/oep-0045-arch-ops-and-config.html from lms.envs.common import ( USE_TZ, ALL_LANGUAGES, ASSET_IGNORE_REGEX, - PARENTAL_CONSENT_AGE_LIMIT, REGISTRATION_EMAIL_PATTERNS_ALLOWED, + PARENTAL_CONSENT_AGE_LIMIT, REGISTRATION_EMAIL_PATTERNS_ALLOWED, REGISTRATION_USERNAME_PATTERNS_ALLOWED, # The following PROFILE_IMAGE_* settings are included as they are # indirectly accessed through the email opt-in API, which is # technically accessible through the CMS via legacy URLs. diff --git a/cms/envs/production.py b/cms/envs/production.py index 8a20a6ba12f..b44a8b70ed8 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -210,6 +210,7 @@ def get_env_setting(setting): SESSION_COOKIE_HTTPONLY = ENV_TOKENS.get('SESSION_COOKIE_HTTPONLY', True) REGISTRATION_EMAIL_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_EMAIL_PATTERNS_ALLOWED') +REGISTRATION_USERNAME_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_USERNAME_PATTERNS_ALLOWED') # allow for environments to specify what cookie name our login subsystem should use # this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can diff --git a/lms/envs/common.py b/lms/envs/common.py index acd86c3e90e..660321dffa8 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3534,6 +3534,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # Optional setting to restrict registration / account creation to only emails # that match a regex in this list. Set to None to allow any email (default). REGISTRATION_EMAIL_PATTERNS_ALLOWED = None +REGISTRATION_USERNAME_PATTERNS_ALLOWED = None ########################## CERTIFICATE NAME ######################## CERT_NAME_SHORT = "Certificate" diff --git a/lms/envs/production.py b/lms/envs/production.py index 787e295113a..aa25b6e02ac 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -167,6 +167,7 @@ def get_env_setting(setting): AWS_SES_REGION_ENDPOINT = ENV_TOKENS.get('AWS_SES_REGION_ENDPOINT', 'email.us-east-1.amazonaws.com') REGISTRATION_EMAIL_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_EMAIL_PATTERNS_ALLOWED') +REGISTRATION_USERNAME_PATTERNS_ALLOWED = ENV_TOKENS.get('REGISTRATION_USERNAME_PATTERNS_ALLOWED') LMS_ROOT_URL = ENV_TOKENS.get('LMS_ROOT_URL') LMS_INTERNAL_ROOT_URL = ENV_TOKENS.get('LMS_INTERNAL_ROOT_URL', LMS_ROOT_URL) diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index 0321250a345..4b0f93ad752 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -135,6 +135,14 @@ def clean(self, value): """ value = self.to_python(value).strip() + + if settings.REGISTRATION_USERNAME_PATTERNS_ALLOWED is not None: + # This Open edX instance has restrictions on what usernames are allowed. + allowed_patterns = settings.REGISTRATION_USERNAME_PATTERNS_ALLOWED + + if not any(re.match(pattern + "$", value) for pattern in allowed_patterns): + raise ValidationError(_(u"Unauthorized username.")) + return super().clean(value) From 797a43a97b6d223ef208c03fc10f4e3863e3ba05 Mon Sep 17 00:00:00 2001 From: Alejandro Cardenas Date: Mon, 23 Nov 2020 08:35:35 -0500 Subject: [PATCH 2/9] fix: changed error messages Username and email error messages updated to match customer's requirements. --- .../core/djangoapps/user_authn/views/registration_form.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index 4b0f93ad752..41a9d8d8414 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -141,7 +141,9 @@ def clean(self, value): allowed_patterns = settings.REGISTRATION_USERNAME_PATTERNS_ALLOWED if not any(re.match(pattern + "$", value) for pattern in allowed_patterns): - raise ValidationError(_(u"Unauthorized username.")) + raise ValidationError(_( + u"Unauthorized username, If you are a Técnico user (student, professor or staff) please register via Técnico-ID" + )) return super().clean(value) @@ -271,7 +273,9 @@ def clean_email(self): # they may have been manually invited by an instructor and if not, # reject the registration. if not CourseEnrollmentAllowed.objects.filter(email=email).exists(): - raise ValidationError(_("Unauthorized email address.")) + raise ValidationError(_( + u"Unauthorized email, If you are a Técnico user (student, professor or staff) please register via Técnico-ID" + )) if email_exists_or_retired(email): raise ValidationError( _( From 6ee6cbeb25af88370c6b995690166f4f63cf3bc2 Mon Sep 17 00:00:00 2001 From: Alejandro Cardenas Date: Tue, 24 Nov 2020 13:30:38 -0500 Subject: [PATCH 3/9] fix: Solved issue with username and email patterns allowed. Added a new function to perform username validation. --- .../user_authn/views/registration_form.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index 41a9d8d8414..6cb52623d50 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -135,16 +135,6 @@ def clean(self, value): """ value = self.to_python(value).strip() - - if settings.REGISTRATION_USERNAME_PATTERNS_ALLOWED is not None: - # This Open edX instance has restrictions on what usernames are allowed. - allowed_patterns = settings.REGISTRATION_USERNAME_PATTERNS_ALLOWED - - if not any(re.match(pattern + "$", value) for pattern in allowed_patterns): - raise ValidationError(_( - u"Unauthorized username, If you are a Técnico user (student, professor or staff) please register via Técnico-ID" - )) - return super().clean(value) @@ -260,6 +250,20 @@ def clean_password(self): raise ValidationError(accounts.AUTHN_PASSWORD_COMPROMISED_MSG) return password + def clean_username(self): + """Enforce username policies (if applicable)""" + username = self.cleaned_data["username"] + if settings.REGISTRATION_USERNAME_PATTERNS_ALLOWED is not None: + # This Open edX instance has restrictions on what usernames are allowed. + allowed_patterns = settings.REGISTRATION_USERNAME_PATTERNS_ALLOWED + + if not any(re.match(pattern + "$", value) for pattern in allowed_patterns): + if not 'social_auth_provider' in self.data: + raise ValidationError(_( + u"Unauthorized username, If you are a Técnico user (student, professor or staff) please register via Técnico-ID" + )) + return username + def clean_email(self): """ Enforce email restrictions (if applicable) """ email = self.cleaned_data["email"] @@ -272,7 +276,7 @@ def clean_email(self): # This email is not on the whitelist of allowed emails. Check if # they may have been manually invited by an instructor and if not, # reject the registration. - if not CourseEnrollmentAllowed.objects.filter(email=email).exists(): + if not CourseEnrollmentAllowed.objects.filter(email=email).exists() and not 'social_auth_provider' in self.data: raise ValidationError(_( u"Unauthorized email, If you are a Técnico user (student, professor or staff) please register via Técnico-ID" )) From 4268ca7213a0124cd46341fd0c4bbfcfbb03c5f7 Mon Sep 17 00:00:00 2001 From: Deimer M Date: Tue, 30 May 2023 20:09:08 -0500 Subject: [PATCH 4/9] feat: Increase randomization bins for python problems This commit is intended to replicate the following commit https://github.com/eduNEXT/edx-platform/commit/8cccc154dc7a9d9f5f3e999d433670b0266d4945 --- common/lib/xmodule/xmodule/capa_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index efff7e9011b..5d557453dfa 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -65,7 +65,7 @@ _ = lambda text: text # Generate this many different variants of problems with rerandomize=per_student -NUM_RANDOMIZATION_BINS = 20 +NUM_RANDOMIZATION_BINS = 200 # Never produce more than this many different seeds, no matter what. MAX_RANDOMIZATION_BINS = 1000 From 35fa77ff176625c25111c279f6314c9e87ebe186 Mon Sep 17 00:00:00 2001 From: DonatoB-eduNEXT Date: Thu, 9 Sep 2021 19:28:59 +0000 Subject: [PATCH 5/9] fix: disable whitelist in the restricted email verification --- openedx/core/djangoapps/user_authn/views/registration_form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index 6cb52623d50..988405dd996 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -276,7 +276,7 @@ def clean_email(self): # This email is not on the whitelist of allowed emails. Check if # they may have been manually invited by an instructor and if not, # reject the registration. - if not CourseEnrollmentAllowed.objects.filter(email=email).exists() and not 'social_auth_provider' in self.data: + if not 'social_auth_provider' in self.data: raise ValidationError(_( u"Unauthorized email, If you are a Técnico user (student, professor or staff) please register via Técnico-ID" )) From d1c3f404b402c0d352790cb99835d0390de02d66 Mon Sep 17 00:00:00 2001 From: Alejandro Cardenas Date: Tue, 24 Nov 2020 14:24:50 -0500 Subject: [PATCH 6/9] fix: fix issue in username clean --- openedx/core/djangoapps/user_authn/views/registration_form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index 988405dd996..3e98f7d75fd 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -257,7 +257,7 @@ def clean_username(self): # This Open edX instance has restrictions on what usernames are allowed. allowed_patterns = settings.REGISTRATION_USERNAME_PATTERNS_ALLOWED - if not any(re.match(pattern + "$", value) for pattern in allowed_patterns): + if not any(re.match(pattern + "$", username) for pattern in allowed_patterns): if not 'social_auth_provider' in self.data: raise ValidationError(_( u"Unauthorized username, If you are a Técnico user (student, professor or staff) please register via Técnico-ID" From 096322b8ccba293d8485deeb4f591144f15056a3 Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Tue, 28 Jun 2022 16:19:32 +0000 Subject: [PATCH 7/9] fix: if pages and resources view is disabled, show all pages in studio (#30550) In a previous PR #28686, the ability to see and enable/disable wiki and progress tabs was removed from studio along with the ability to re-order non-static tabs. The ability to toggle the Wiki tab was moved to the pages and resources section of the course authoring MFE. If that MFE is unavailable this means there is no way to show/hide the Wiki. This reverts some of the old changes if the pages and resources view is disabled. --- .../rest_api/v0/tests/test_tabs.py | 35 ++------ .../contentstore/rest_api/v0/views/tabs.py | 6 +- cms/djangoapps/contentstore/views/tabs.py | 21 ++--- .../contentstore/views/tests/test_tabs.py | 20 ++--- cms/static/images/preview-lms-staticpage.png | Bin 3214 -> 0 bytes cms/templates/edit-tabs.html | 80 ++++++++++++++++-- common/lib/xmodule/xmodule/tabs.py | 3 +- 7 files changed, 104 insertions(+), 61 deletions(-) delete mode 100644 cms/static/images/preview-lms-staticpage.png diff --git a/cms/djangoapps/contentstore/rest_api/v0/tests/test_tabs.py b/cms/djangoapps/contentstore/rest_api/v0/tests/test_tabs.py index 5162b50b068..5daf2f55a0a 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/tests/test_tabs.py +++ b/cms/djangoapps/contentstore/rest_api/v0/tests/test_tabs.py @@ -43,15 +43,11 @@ def setUp(self): ) # add a static tab to the course, for code coverage - # 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.test_tab = ItemFactory.create( + parent_location=self.course.location, + category="static_tab", + display_name="Static_1", + ) self.reload_course() def check_invalid_response(self, resp): @@ -129,27 +125,6 @@ def test_reorder_static_tabs(self): new_tab_ids = [tab.tab_id for tab in self.course.tabs] assert new_tab_ids == reordered_tab_ids - def test_reorder_tabs_invalid_list(self): - """ - Test re-ordering of tabs with invalid tab list. - - Not all tabs can be rearranged. Here we are trying to swap the first - two tabs, which is disallowed since the first tab is the "Course" tab - which is immovable. - """ - - orig_tab_ids = [tab.tab_id for tab in self.course.tabs] - tab_ids = list(orig_tab_ids) - - # reorder the first two tabs - tab_ids[0], tab_ids[1] = tab_ids[1], tab_ids[0] - - # post the request - resp = self.make_reorder_tabs_request([{"tab_id": tab_id} for tab_id in tab_ids]) - assert resp.status_code == 400 - error = self.check_invalid_response(resp) - assert "error" in error - def test_reorder_tabs_invalid_tab_ids(self): """ Test re-ordering of tabs with invalid tab. diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/tabs.py b/cms/djangoapps/contentstore/rest_api/v0/views/tabs.py index ce5e72de434..518381a85a5 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/views/tabs.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/tabs.py @@ -13,7 +13,7 @@ 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_static_tabs, reorder_tabs_handler +from ....views.tabs import edit_tab_handler, get_course_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 static tabs in a course including hidden tabs. + Get a list of all the tabs in a course including hidden tabs. **Example Request** @@ -82,7 +82,7 @@ def get(self, request: Request, course_id: str) -> Response: self.permission_denied(request) course_module = modulestore().get_course(course_key) - tabs_to_render = get_course_static_tabs(course_module, request.user) + tabs_to_render = get_course_tabs(course_module, request.user) return Response(CourseTabSerializer(tabs_to_render, many=True).data) diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py index 5482be63dc8..384bcb1270b 100644 --- a/cms/djangoapps/contentstore/views/tabs.py +++ b/cms/djangoapps/contentstore/views/tabs.py @@ -19,7 +19,7 @@ from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.student.auth import has_course_author_access from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest, expect_json -from ..utils import get_lms_link_for_item +from ..utils import get_lms_link_for_item, get_pages_and_resources_url __all__ = ["tabs_handler", "update_tabs_handler"] @@ -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 and select only static tabs (a.k.a. user-created tabs) + # get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs # present in the same order they are displayed in LMS - tabs_to_render = list(get_course_static_tabs(course_item, request.user)) + tabs_to_render = list(get_course_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_static_tabs(course_item: CourseBlock, user: User) -> Iterable[CourseTab]: +def get_course_tabs(course_item: CourseBlock, user: User) -> Iterable[CourseTab]: """ - Yields all the static tabs in a course including hidden tabs. + Yields all the course tabs in a course including hidden tabs. Args: course_item (CourseBlock): The course object from which to get the tabs @@ -90,12 +90,14 @@ def get_course_static_tabs(course_item: CourseBlock, user: User) -> Iterable[Cou Iterable[CourseTab]: An iterable containing course tab objects from the course """ - + pages_and_resources_mfe_enabled = bool(get_pages_and_resources_url(course_item.id)) for tab in CourseTabList.iterate_displayable(course_item, user=user, inline_collections=False, include_hidden=True): if isinstance(tab, StaticTab): # 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 + # If the course apps MFE is set up and pages and resources is enabled, then only show static tabs + if isinstance(tab, StaticTab) or not pages_and_resources_mfe_enabled: yield tab @@ -119,7 +121,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 static tabs request + Helper function for handling reorder of tabs request """ # Static tabs are identified by locators (a UsageKey) instead of a tab id like @@ -152,9 +154,8 @@ def create_new_list(tab_locators, old_tab_list): tab = get_tab_by_tab_id_locator(old_tab_list, tab_locator) if tab is None: raise ValidationError({"error": f"Tab with id_locator '{tab_locator}' does not exist."}) - if not isinstance(tab, StaticTab): - raise ValidationError({"error": f"Cannot reorder tabs of type '{tab.type}'"}) - new_tab_list.append(tab) + if isinstance(tab, StaticTab): + new_tab_list.append(tab) # the old_tab_list may contain additional tabs that were not rendered in the UI because of # global or course settings. so add those to the end of the list. diff --git a/cms/djangoapps/contentstore/views/tests/test_tabs.py b/cms/djangoapps/contentstore/views/tests/test_tabs.py index e366420d224..08d832bd628 100644 --- a/cms/djangoapps/contentstore/views/tests/test_tabs.py +++ b/cms/djangoapps/contentstore/views/tests/test_tabs.py @@ -1,5 +1,6 @@ """ Tests for tab functions (just primitive). """ + import json import random @@ -26,15 +27,12 @@ def setUp(self): # Set the URL for tests self.url = reverse_course_url('tabs_handler', self.course.id) - # 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) + # 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" + ) self.reload_course() def check_invalid_tab_id_response(self, resp): @@ -95,7 +93,7 @@ def test_reorder_tabs(self): # Remove one tab randomly. This shouldn't delete the tab. tabs_data.pop() - # post the request with the reordered static tabs only + # post the request resp = self.client.ajax_post( self.url, data={ @@ -167,7 +165,7 @@ def test_tab_preview_html(self): """ Verify that the static tab renders itself with the correct HTML """ - preview_url = f'/xblock/{self.test_tabs[0].location}/{STUDENT_VIEW}' + preview_url = f'/xblock/{self.test_tab.location}/{STUDENT_VIEW}' resp = self.client.get(preview_url, HTTP_ACCEPT='application/json') assert resp.status_code == 200 diff --git a/cms/static/images/preview-lms-staticpage.png b/cms/static/images/preview-lms-staticpage.png deleted file mode 100644 index 7940a106bf47f02991988c1bda329d209ec371cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3214 zcmV;93~}>`P)-+or`uh6c-``+hU_d}XARr)6P*A_Wzi)4EkdTm{pPxZMK!AXNprD|Ce}6we zKL7v#nVFe_f`ZS_&+qT=5D*YBFfdF^Os}u6JUl!vFE3wTUyqNEA0Hohcz9V^Sq~2n zPft$(00D4ta8XcDaB*;`sHjj;P=A1bO-xLHe}6A8FdrZvFfT9B(a{eO55T~{U|(Nf zUtm8!K!1OL0096`Pf$NVKOY|;0RR9XAR)cIy?k|S+1cj%{{BEeKTuCkKtMsBo}NBF zK59=cYN6GBetzuN$grY_US4d{@cZiO>f77f?AODvu)5Ob_MDuYPEJm3ZEgSk{r~*@ zjEs!@#>W1-y8pw&|JT?5=H~zU`u+F!{^{xd%F6!0!2jCX|L*SXP298q01DHFeYCjr*d|S{Y_jb(>3ye1+NSsY|NjA<8A-MQ1KxKm zFZ=Z6mj||`(Kn+{BZ&nu=HYQc1dL54j7{O~M?U(okADJQCzs~r;o;%o;o;%&a1r4D zfJqRzOAn7r0>lV#!i0aUoo*Z2674BU+ z=}kNN*~|XZ8Qn&ktQDk9Vaa6y?uvM?e%Lk{nLFrruvvFc;mj&Ixa^s;BbUP-3 zOQ+tz!VdGxlKIde=n|(9rtRg+sdEsw(4FLFPr)uN&y0F+-oTA8af51lX(smz7>&JI z*#We=-)%uJOT91!&unW}Q&wMh_enQ}#3^_-xoGNjgNjne16Hn^cuuPBY=nZzor@N; z-NlogQSj6*l^KQa5nM0OahqkTG`l6wJYH_J%hTK4+&BfFQ12aX zr#xP6*d%wSlk$LqeNOeb8D8izGVuQb<>BGs;o;%o@e+d1!^KiuuWl5E;iDTfkgtT{ z>eb$jn2)Iy+FC-5D4mS)@ROfv-wHYKJU(a%nTll4SiF0ghIm7V(E+pGd-MsxhE1M> zUWHls>iKd6K3^}wHY#y^P@orA`RV7gwvYTXV;GI^%Z7rRSC@u7eLZAIyXMJcK9Nt8xdYJ8=>O`^u3bH;h(XY+HC2&hzPqKR5 zh;aiLQIuTgxx2clWOuFYW1M6^KQeKu68}Op)9n%mC{mZ;xs;*Q+i)pq@UDh?TtR8M zU@~2WZK*0;<&yDLx{S~!CSkdJWy_hEhx0*FMhSJy^rH|t$Yj0}Xm1Pl&e!5VQ7pqP zr@|{}8k^+IvacW+n-QGP(Gr*Waz3nblx&>NzaSV77b|(-SEQ&hG!Cu-Jz`4jRZD7L zR4!C;vRZML&X+dpiKK+8MWsxLL6+bxF=NOK-J2?6afEX_z3JlQH+HYarh-9 zkPgiO-G~Cuu@+4M;ENOx2NMQXeR*qq*NN>?@DIWg$B(U>ODZ6j+g!D3kY7O#EoAyTH^ z*!63pjH1A}0M}$nOtC%Hz*7!UtAxn`z(NQufL9UQ>$G5LjrEZqWVN_~j-{)ZcpF&B zHo!Gyyq2O2X*P=?Nh>h_wXcIX%EO#!djp}N7}mnBg?7Y330^4*0~nIE(duTIn!=ID z1u;jaRDk0Yme;CP17_Kx1Q(CaRrT^4Eqi6mLC+gM1WDFfMjz^BB${u68;!`js9`X* z<#`IJ8*C-Wl_#BKQKJP7=IUGcb}-YaaJ9`OQ&3l8g*vC<7NKO}wW)A4v)7i?Qhm%{ zBfdPNQje60b=6R-BWgjI#DYp~NZTaYHwAl>*vE4!4JPragt8mfCWSqaWZ{X*jrD!M z%1*LMI*ThLIiL{=yqe4g&Q7lyNW=4on+HG z!-YyhnWBZ2EVc&yivi38$qH&v8il^6;e3d~_7Sa1RnG|i9~D9y&`K-;OEOH4%O zM(b@WS=6Wzp}?(aVIg_EL83iuW#C92{fQt>j z4raQZWH}H_!BYkMpV~?`2Ag06g;Z2%AsS2%*j74+by5V5Hi^mjw$%}}AWT#$V$Ga3 zk{e_qEPHC4CP|zdVWu7@S?MIpzhnKXn0}^cwu5D=aZQ7KDB2oU4tkOAN*2d%*lvpO zc4}tMn)V08FH5mOwI$xBTwk(sK>^gbtj)eVQSZF_J>2is!(GV}-0{Q|Ist)!b(vmR z$zm94p>e{uU5T=f3L9r9SttS&re!;1y^lOB!n+ zS!enCU6$3gq+2d(5JcBqr;*oLxPeOLVqN2oZW9)Tl`Kr`XccbJqeJv0i={A?@*gA$ zw_Mb55!8Hx>MNaY!XFM>iIXHzC3a4|UX%%%fOKz56$Pl_#Ey9|Z zC`>ZIzQSeAq9l##V*O0%k&=bQjN0)FnkX9FLxXX+ubV6cy&+Q);|3QoJCX%{LN|+b zVc6T-*m@2oUkm1(b73 z*-F+nCLky_i4|G+%y#&?a*~Aw#2Ve@udfSOxjQxB_A9T$@Ob`%mKSI(-mFVn%hpsl zk;MXHj@vJ5xwkN&Td);+VxtDZ6qe^XxDPh@&dbh|Q6KWQg?g*>l*Xm`R5U$rWhW%2 z)e*JYcANoI#j@|zc|1v+1mEGN9Un1N=vd`lr27_*f3|EL&rfR#IAO9@G z7aigl44wTF+-xO#T*Y*zm^-b+OV-08p#RL

${_("Content")} ## Translators: Custom Pages refer to the tabs that appear in the top navigation of each course. - > ${_("Custom pages")} + > ${_("Pages")}

% endif @@ -101,8 +101,59 @@

${_("Page Actions")}

css_class = css_class + " is-fixed" %> -
  • - + % if isinstance(tab, StaticTab): +
  • + + % else: +
  • +
    + + % if tab.is_collection: + +

    ${_(tab.name)}

    +
      + % for item in tab.items(context_course): +
    • + ${_(item.name)} +
    • + % endfor +
    + + % else: +

    ${_(tab.name)}

    + % endif +
    + +
    +
      + + % if tab.is_hideable: +
    • + + % if tab.is_hidden: + + % else: + + % endif +
      +
    • + % endif + +
    +
    + + % if tab.is_movable: +
    + ${_("Drag to reorder")} +
    + % else: +
    + ${_("This page cannot be reordered")} +
    + % endif +
  • + + % endif % endfor
  • @@ -116,6 +167,22 @@

    ${_("Page Actions")}

    + % if pages_and_resources_mfe_enabled: + + % else: + % endif
    -

    ${_("Custom pages in your course")}

    +

    ${_("Pages in Your Course")}

    - ${_('Preview of Pages in your course')} -
    ${_("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.")}
    + ${_('Preview of Pages in your course')} +
    ${_("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.")}
    diff --git a/common/lib/xmodule/xmodule/tabs.py b/common/lib/xmodule/xmodule/tabs.py index 1292db60ac2..3d7bdb70d7c 100644 --- a/common/lib/xmodule/xmodule/tabs.py +++ b/common/lib/xmodule/xmodule/tabs.py @@ -53,7 +53,7 @@ class CourseTab(metaclass=ABCMeta): priority = None # Class property that specifies whether the tab can be moved within a course's list of tabs - is_movable = True + is_movable = False # Class property that specifies whether the tab is a collection of other tabs is_collection = False @@ -301,6 +301,7 @@ class StaticTab(CourseTab): """ type = 'static_tab' is_default = False # A static tab is never added to a course by default + is_movable = True allow_multiple = True priority = 100 From a3a4fff4a3fbcdff53c9056cadfeb9e80406ad63 Mon Sep 17 00:00:00 2001 From: magajh Date: Fri, 22 Oct 2021 09:37:52 -0400 Subject: [PATCH 8/9] LD-9: Remove preview and get urls for org --- cms/djangoapps/contentstore/utils.py | 11 +---------- cms/djangoapps/contentstore/views/assets.py | 7 ++++++- cms/templates/container.html | 2 +- common/djangoapps/util/course.py | 8 +++++++- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 1e0d3e60c62..dff6833bd83 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -104,7 +104,7 @@ def _remove_instructors(course_key): log.error(f"Error in deleting course groups for {course_key}: {err}") -def get_lms_link_for_item(location, preview=False): +def get_lms_link_for_item(location, preview=False): # pylint: disable=unused-argument """ Returns an LMS link to the course with a jump_to to the provided location. @@ -124,15 +124,6 @@ def get_lms_link_for_item(location, preview=False): if lms_base is None: return None - if preview: - # checks PREVIEW_LMS_BASE value in site configuration for the given course_org_filter(org) - # if not found returns settings.FEATURES.get('PREVIEW_LMS_BASE') - lms_base = SiteConfiguration.get_value_for_org( - location.org, - "PREVIEW_LMS_BASE", - settings.FEATURES.get('PREVIEW_LMS_BASE') - ) - return "//{lms_base}/courses/{course_key}/jump_to/{location}".format( lms_base=lms_base, course_key=str(location.course_key), diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 25fd7b955b1..ebb5d425306 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -593,7 +593,12 @@ def _get_asset_json(display_name, content_type, date, location, thumbnail_locati Helper method for formatting the asset information to send to client. ''' asset_url = StaticContent.serialize_asset_key_with_slash(location) - external_url = urljoin(configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL), asset_url) + lms_root = configuration_helpers.get_value_for_org( + location.org, + 'LMS_ROOT_URL', + settings.LMS_ROOT_URL + ) + external_url = urljoin(lms_root, asset_url) return { 'display_name': display_name, 'content_type': content_type, diff --git a/cms/templates/container.html b/cms/templates/container.html index 17de3d20ea7..ffafa8d96e2 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -138,7 +138,7 @@

    ${_("Page Actions")}

    ${_("View Live Version")}
    -