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

Jf/updated xblock axim #814

Merged
merged 9 commits into from
Apr 2, 2024
11 changes: 1 addition & 10 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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),
Expand Down
5 changes: 4 additions & 1 deletion cms/djangoapps/contentstore/views/tests/test_course_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,10 @@ def test_verify_warn_only_on_enabled_blocks(self, enabled_block_types, deprecate
expected_block_types
)

@override_settings(FEATURES={'ENABLE_EXAM_SETTINGS_HTML_VIEW': True})
@override_settings(FEATURES={
'ENABLE_EXAM_SETTINGS_HTML_VIEW': True,
'ENABLE_MFE_FOR_TESTING': True
})
@patch('cms.djangoapps.models.settings.course_metadata.CourseMetadata.validate_proctoring_settings')
def test_proctoring_link_is_visible(self, mock_validate_proctoring_settings):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
FEATURES_WITH_EXAM_SETTINGS_ENABLED = FEATURES_WITH_CERTS_ENABLED.copy()
FEATURES_WITH_EXAM_SETTINGS_ENABLED['ENABLE_EXAM_SETTINGS_HTML_VIEW'] = True
FEATURES_WITH_EXAM_SETTINGS_ENABLED['ENABLE_PROCTORED_EXAMS'] = True
FEATURES_WITH_EXAM_SETTINGS_ENABLED['ENABLE_MFE_FOR_TESTING'] = True

FEATURES_WITH_EXAM_SETTINGS_DISABLED = FEATURES_WITH_CERTS_ENABLED.copy()
FEATURES_WITH_EXAM_SETTINGS_DISABLED['ENABLE_EXAM_SETTINGS_HTML_VIEW'] = False
Expand Down Expand Up @@ -179,7 +180,10 @@ def test_exam_settings_alert_not_shown(self, page_handler):
alert_nodes = parsed_html.find_class('exam-settings-alert')
assert len(alert_nodes) == 0

@override_settings(FEATURES={'ENABLE_EXAM_SETTINGS_HTML_VIEW': True})
@override_settings(FEATURES={
'ENABLE_EXAM_SETTINGS_HTML_VIEW': True,
'ENABLE_MFE_FOR_TESTING': True
})
@patch('cms.djangoapps.models.settings.course_metadata.CourseMetadata.validate_proctoring_settings')
def test_proctoring_link_is_visible(self, mock_validate_proctoring_settings):

Expand Down
26 changes: 26 additions & 0 deletions cms/lib/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Helper methods for the CMS.
"""

from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from edx_toggles.toggles import SettingDictToggle


def use_course_authoring_mfe(org) -> bool:
"""
Checks with the org if the tenant enables the
Course Authoring MFE.
Returns:
True if the MFE setting is activated, by default
the MFE is deactivated
"""

ENABLE_MFE_FOR_TESTING = SettingDictToggle(
"FEATURES", "ENABLE_MFE_FOR_TESTING", default=False, module_name=__name__
).is_enabled()

use_microfrontend = configuration_helpers.get_value_for_org(
org, "ENABLE_COURSE_AUTHORING_MFE", ENABLE_MFE_FOR_TESTING or False
)

return bool(use_microfrontend)
2 changes: 1 addition & 1 deletion cms/templates/container.html
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ <h3 class="sr">${_("Page Actions")}</h3>
<span class="action-button-text">${_("View Live Version")}</span>
</a>
</li>
<li class="action-item action-preview nav-item">
<li class="action-item action-preview nav-item" style="display: none">
<a href="${draft_preview_link}" class="button button-preview action-button" rel="external" title="${_('Preview the courseware in the LMS')}">
<span class="action-button-text">${_("Preview")}</span>
</a>
Expand Down
7 changes: 4 additions & 3 deletions cms/templates/studio_xblock_wrapper.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
dump_js_escaped_json, js_escaped_string
)
from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor
from cms.lib.utils import use_course_authoring_mfe
%>
<%
use_new_editor_text = use_new_text_editor()
use_new_editor_video = use_new_video_editor()
use_new_editor_problem = use_new_problem_editor()
use_new_editor_text = use_course_authoring_mfe(xblock.location.course_key.org) and use_new_text_editor()
use_new_editor_video = use_course_authoring_mfe(xblock.location.course_key.org) and use_new_video_editor()
use_new_editor_problem = use_course_authoring_mfe(xblock.location.course_key.org) and use_new_problem_editor()
xblock_url = xblock_studio_url(xblock)
show_inline = xblock.has_children and not xblock_url
section_class = "level-nesting" if show_inline else "level-element"
Expand Down
3 changes: 2 additions & 1 deletion cms/templates/widgets/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from cms.djangoapps.contentstore.utils import get_pages_and_resources_url
from openedx.core.djangoapps.discussions.config.waffle import ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND
from openedx.core.djangoapps.lang_pref.api import header_language_selector_is_enabled, released_languages
from cms.lib.utils import use_course_authoring_mfe
%>
<div class="wrapper-header wrapper" id="view-top">
<header class="primary" role="banner">
Expand Down Expand Up @@ -45,7 +46,7 @@ <h1 class="branding">
if settings.FEATURES.get("CERTIFICATES_HTML_VIEW") and context_course.cert_html_view_enabled:
certificates_url = reverse('certificates_list_handler', kwargs={'course_key_string': six.text_type(course_key)})
checklists_url = reverse('checklists_handler', kwargs={'course_key_string': six.text_type(course_key)})
pages_and_resources_mfe_enabled = ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND.is_enabled(context_course.id)
pages_and_resources_mfe_enabled = use_course_authoring_mfe(course_key.org) and ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND.is_enabled(context_course.id)
%>
<h2 class="info-course">
<span class="sr">${_("Current Course:")}</span>
Expand Down
96 changes: 94 additions & 2 deletions common/djangoapps/student/views/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import Signal, receiver # lint-amnesty, pylint: disable=unused-import
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect

from django.shortcuts import redirect
from django.template.context_processors import csrf
from django.urls import reverse
Expand All @@ -34,6 +35,8 @@
# Note that this lives in LMS, so this dependency should be refactored.
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx_filters.exceptions import OpenEdxFilterException
from openedx_filters.tooling import OpenEdxPublicFilter
from pytz import UTC
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
Expand Down Expand Up @@ -114,6 +117,79 @@ def csrf_token(context):
' name="csrfmiddlewaretoken" value="{}" /></div>').format(Text(token)))


class HomepageRenderStarted(OpenEdxPublicFilter):
"""
Custom class used to create homepage render filters and its custom methods.
"""

filter_type = "org.openedx.learning.homepage.render.started.v1"

class RedirectToPage(OpenEdxFilterException):
"""
Custom class used to stop the homepage rendering process.
"""

def __init__(self, message, redirect_to=""):
"""
Override init that defines specific arguments used in the homepage render process.

Arguments:
message: error message for the exception.
redirect_to: URL to redirect to.
"""
super().__init__(message, redirect_to=redirect_to)

class RenderInvalidHomepage(OpenEdxFilterException):
"""
Custom class used to stop the homepage render process.
"""

def __init__(self, message, index_template="", template_context=None):
"""
Override init that defines specific arguments used in the index render process.

Arguments:
message: error message for the exception.
index_template: template path rendered instead.
template_context: context used to the new index_template.
"""
super().__init__(
message,
index_template=index_template,
template_context=template_context,
)

class RenderCustomResponse(OpenEdxFilterException):
"""
Custom class used to stop the homepage rendering process.
"""

def __init__(self, message, response=None):
"""
Override init that defines specific arguments used in the homepage render process.

Arguments:
message: error message for the exception.
response: custom response which will be returned by the homepage view.
"""
super().__init__(
message,
response=response,
)

@classmethod
def run_filter(cls, context, template_name):
"""
Execute a filter with the signature specified.

Arguments:
context (dict): context dictionary for homepage template.
template_name (str): template name to be rendered by the homepage.
"""
data = super().run_pipeline(context=context, template_name=template_name)
return data.get("context"), data.get("template_name")


# NOTE: This view is not linked to directly--it is called from
# branding/views.py:index(), which is cached for anonymous users.
# This means that it should always return the same thing for anon
Expand Down Expand Up @@ -168,7 +244,23 @@ def index(request, extra_context=None, user=AnonymousUser()):
# Add marketable programs to the context.
context['programs_list'] = get_programs_with_type(request.site, include_hidden=False)

return render_to_response('index.html', context)
index_template = 'index.html'
try:
# .. filter_implemented_name: HomepageRenderStarted
# .. filter_type: org.openedx.learning.homepage.render.started.v1
context, index_template = HomepageRenderStarted.run_filter(
context=context, template_name=index_template,
)
except HomepageRenderStarted.RenderInvalidHomepage as exc:
response = render_to_response(exc.index_template, exc.template_context) # pylint: disable=no-member
except HomepageRenderStarted.RedirectToPage as exc:
response = HttpResponseRedirect(exc.redirect_to or reverse('account_settings'))
except HomepageRenderStarted.RenderCustomResponse as exc:
response = exc.response # pylint: disable=no-member
else:
response = render_to_response(index_template, context)

return response


def compose_activation_email(user, user_registration=None, route_enabled=False, profile_name='', redirect_url=None):
Expand Down
8 changes: 7 additions & 1 deletion common/djangoapps/util/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@ def get_link_for_about_page(course):
elif settings.FEATURES.get('ENABLE_MKTG_SITE') and getattr(course, 'marketing_url', None):
course_about_url = course.marketing_url
else:
about_base = configuration_helpers.get_value_for_org(
course.id.org,
'LMS_ROOT_URL',
settings.LMS_ROOT_URL
)

course_about_url = '{about_base_url}/courses/{course_key}/about'.format(
about_base_url=configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL),
about_base_url=about_base,
course_key=str(course.id),
)

Expand Down
16 changes: 16 additions & 0 deletions lms/djangoapps/certificates/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,19 @@ def test_html_view_site_configuration_missing(self):
response,
'This should not survive being overwritten by static content',
)

@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED, GOOGLE_ANALYTICS_4_ID='GA-abc')
@with_site_configuration(configuration={'platform_name': 'My Platform Site'})
def test_html_view_with_g4(self):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=str(self.course.id),
uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=2)
response = self.client.get(test_url)
self.assertContains(
response,
'awarded this My Platform Site Honor Code Certificate of Completion',
)
self.assertContains(response, 'googletagmanager')
12 changes: 10 additions & 2 deletions lms/djangoapps/courseware/block_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from edx_proctoring.api import get_attempt_status_summary
from edx_proctoring.services import ProctoringService
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.permissions import JwtRestrictedApplication
from edx_when.field_data import DateLookupFieldData
from eventtracking import tracker
from opaque_keys import InvalidKeyError
Expand Down Expand Up @@ -841,12 +842,19 @@ def handle_xblock_callback(request, course_id, usage_id, handler, suffix=None):
)
else:
if user_auth_tuple is not None:
request.user, _ = user_auth_tuple
# When using JWT authentication, the second element contains the JWT token. We need it to determine
# whether the application that issued the token is restricted.
request.user, request.auth = user_auth_tuple
# This is verified by the `JwtRestrictedApplication` before it decodes the token.
request.successful_authenticator = authenticator
break

# NOTE (CCB): Allow anonymous GET calls (e.g. for transcripts). Modifying this view is simpler than updating
# the XBlocks to use `handle_xblock_callback_noauth`, which is practically identical to this view.
if request.method != 'GET' and not (request.user and request.user.is_authenticated):
# Block all request types coming from restricted applications.
if (
request.method != 'GET' and not (request.user and request.user.is_authenticated)
) or JwtRestrictedApplication().has_permission(request, None): # type: ignore
return HttpResponseForbidden('Unauthenticated')

request.user.known = request.user.is_authenticated
Expand Down
22 changes: 21 additions & 1 deletion lms/djangoapps/courseware/tests/test_block_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
from openedx.core.djangoapps.credit.api import set_credit_requirement_status, set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
from openedx.core.djangoapps.oauth_dispatch.jwt import _create_jwt, create_jwt_for_user
from openedx.core.djangoapps.oauth_dispatch.tests.factories import AccessTokenFactory, ApplicationFactory
from openedx.core.lib.courses import course_image_url
from openedx.core.lib.gating import api as gating_api
Expand Down Expand Up @@ -375,6 +375,26 @@ def test_jwt_authentication(self):
response = self.client.post(dispatch_url, {}, **headers)
assert 200 == response.status_code

def test_jwt_authentication_with_restricted_application(self):
"""Test that the XBlock endpoint disallows JWT authentication with restricted applications."""

def _mock_create_restricted_jwt(*args, **kwargs):
"""Pass an additional argument to `_create_jwt` without modifying the signature of `create_jwt_for_user`."""
kwargs['is_restricted'] = True
return _create_jwt(*args, **kwargs)

with patch('openedx.core.djangoapps.oauth_dispatch.jwt._create_jwt', _mock_create_restricted_jwt):
token = create_jwt_for_user(self.mock_user)

dispatch_url = self._get_dispatch_url()
headers = {'HTTP_AUTHORIZATION': 'JWT ' + token}

response = self.client.get(dispatch_url, {}, **headers)
assert 403 == response.status_code

response = self.client.post(dispatch_url, {}, **headers)
assert 403 == response.status_code

def test_missing_position_handler(self):
"""
Test that sending POST request without or invalid position argument don't raise server error
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/courseware/tests/test_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ def test_tab_link(self, toggle_enabled):
else:
expected_link = reverse("forum_form_discussion", args=[str(self.course.id)])

with self.settings(FEATURES={'ENABLE_DISCUSSION_SERVICE': True}):
with self.settings(FEATURES={'ENABLE_DISCUSSION_SERVICE': True, 'ENABLE_MFE_FOR_TESTING': True}):
with override_waffle_flag(ENABLE_DISCUSSIONS_MFE, toggle_enabled):
self.check_discussion(
tab_list=self.tabs_with_discussion,
Expand Down
Loading
Loading