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

feat: [AXM-355] add information about offline metadata to course blocks #2571

Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: [AXM-355] add information about offline metadata to course blocks
NiedielnitsevIvan authored and GlugovGrGlib committed Jun 11, 2024
commit 05deb315fc95f1ab89472e06b79109a17e00e2e2
274 changes: 143 additions & 131 deletions lms/djangoapps/courseware/views/views.py
Original file line number Diff line number Diff line change
@@ -1537,139 +1537,151 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True, disable_sta
Returns an HttpResponse with HTML content for the xBlock with the given usage_key.
The returned HTML is a chromeless rendering of the xBlock (excluding content of the containing courseware).
"""
# usage_key = UsageKey.from_string(usage_key_string)
#
# usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
# course_key = usage_key.course_key
#
# # Gathering metrics to make performance measurements easier.
# set_custom_attributes_for_course_key(course_key)
# set_custom_attribute('usage_key', usage_key_string)
# set_custom_attribute('block_type', usage_key.block_type)
#
# requested_view = request.GET.get('view', 'student_view')
# if requested_view != 'student_view' and requested_view != 'public_view': # lint-amnesty, pylint: disable=consider-using-in
# return HttpResponseBadRequest(
# f"Rendering of the xblock view '{bleach.clean(requested_view, strip=True)}' is not supported."
# )
#
# staff_access = has_access(request.user, 'staff', course_key)
#
# with modulestore().bulk_operations(course_key):
# # verify the user has access to the course, including enrollment check
# try:
# course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=check_if_enrolled)
# except CourseAccessRedirect:
# raise Http404("Course not found.") # lint-amnesty, pylint: disable=raise-missing-from
#
# # with course access now verified:
# # assume masquerading role, if applicable.
# # (if we did this *before* the course access check, then course staff
# # masquerading as learners would often be denied access, since course
# # staff are generally not enrolled, and viewing a course generally
# # requires enrollment.)
# _course_masquerade, request.user = setup_masquerade(
# request,
# course_key,
# staff_access,
# )
#
# # Record user activity for tracking progress towards a user's course goals (for mobile app)
# UserActivity.record_user_activity(
# request.user, usage_key.course_key, request=request, only_if_mobile_app=True
# )
#
# # get the block, which verifies whether the user has access to the block.
# recheck_access = request.GET.get('recheck_access') == '1'
# block, _ = get_block_by_usage_id(
# request,
# str(course_key),
# str(usage_key),
# disable_staff_debug_info=disable_staff_debug_info,
# course=course,
# will_recheck_access=recheck_access,
# )
#
# student_view_context = request.GET.dict()
# student_view_context['show_bookmark_button'] = request.GET.get('show_bookmark_button', '0') == '1'
# student_view_context['show_title'] = request.GET.get('show_title', '1') == '1'
#
# is_learning_mfe = is_request_from_learning_mfe(request)
# # Right now, we only care about this in regards to the Learning MFE because it results
# # in a bad UX if we display blocks with access errors (repeated upgrade messaging).
# # If other use cases appear, consider removing the is_learning_mfe check or switching this
# # to be its own query parameter that can toggle the behavior.
# student_view_context['hide_access_error_blocks'] = is_learning_mfe and recheck_access
# is_mobile_app = is_request_from_mobile_app(request)
# student_view_context['is_mobile_app'] = is_mobile_app
#
# enable_completion_on_view_service = False
# completion_service = block.runtime.service(block, 'completion')
# if completion_service and completion_service.completion_tracking_enabled():
# if completion_service.blocks_to_mark_complete_on_view({block}):
# enable_completion_on_view_service = True
# student_view_context['wrap_xblock_data'] = {
# 'mark-completed-on-view-after-delay': completion_service.get_complete_on_view_delay_ms()
# }
#
# missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
#
# # Some content gating happens only at the Sequence level (e.g. "has this
# # timed exam started?").
# ancestor_sequence_block = enclosing_sequence_for_gating_checks(block)
# if ancestor_sequence_block:
# context = {'specific_masquerade': is_masquerading_as_specific_student(request.user, course_key)}
# # If the SequenceModule feels that gating is necessary, redirect
# # there so we can have some kind of error message at any rate.
# if ancestor_sequence_block.descendants_are_gated(context):
# return redirect(
# reverse(
# 'render_xblock',
# kwargs={'usage_key_string': str(ancestor_sequence_block.location)}
# )
# )
#
# # For courses using an LTI provider managed by edx-exams:
# # Access to exam content is determined by edx-exams and passed to the LMS using a
# # JWT url param. There is no longer a need for exam gating or logic inside the
# # sequence block or its render call. descendants_are_gated shoule not return true
# # for these timed exams. Instead, sequences are assumed gated by default and we look for
# # an access token on the request to allow rendering to continue.
# if course.proctoring_provider == 'lti_external':
# seq_block = ancestor_sequence_block if ancestor_sequence_block else block
# if getattr(seq_block, 'is_time_limited', None):
# if not _check_sequence_exam_access(request, seq_block.location):
# return HttpResponseForbidden("Access to exam content is restricted")
#
# fragment = block.render(requested_view, context=student_view_context)
# optimization_flags = get_optimization_flags_for_content(block, fragment)
#
# context = {
# 'fragment': fragment,
# 'course': course,
# 'block': block,
# 'disable_accordion': True,
# 'allow_iframing': True,
# 'disable_header': True,
# 'disable_footer': True,
# 'disable_window_wrap': True,
# 'enable_completion_on_view_service': enable_completion_on_view_service,
# 'edx_notes_enabled': is_feature_enabled(course, request.user),
# 'staff_access': staff_access,
# 'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
# 'missed_deadlines': missed_deadlines,
# 'missed_gated_content': missed_gated_content,
# 'has_ended': course.has_ended(),
# 'web_app_course_url': get_learning_mfe_home_url(course_key=course.id, url_fragment='home'),
# 'on_courseware_page': True,
# 'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
# 'is_learning_mfe': is_learning_mfe,
# 'is_mobile_app': is_mobile_app,
# 'render_course_wide_assets': True,
#
# **optimization_flags,
# }
from django.http import Http404, HttpResponse
from opaque_keys.edx.keys import CourseKey, UsageKey
from xmodule.modulestore.django import modulestore
from openedx.features.offline_mode.renderer import XBlockRenderer
from openedx.features.offline_mode.utils import generate_offline_content

usage_key_string = 'block-v1:OpenedX+DemoX+DemoCourse+type@problem+block@c89f56c74a3a424dbffb665d4643b42f'
usage_key = UsageKey.from_string(usage_key_string)
xblock = modulestore().get_item(usage_key)
html_data = XBlockRenderer(usage_key_string).render_xblock_from_lms()
generate_offline_content(xblock, html_data)

usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
course_key = usage_key.course_key

# Gathering metrics to make performance measurements easier.
set_custom_attributes_for_course_key(course_key)
set_custom_attribute('usage_key', usage_key_string)
set_custom_attribute('block_type', usage_key.block_type)

requested_view = request.GET.get('view', 'student_view')
if requested_view != 'student_view' and requested_view != 'public_view': # lint-amnesty, pylint: disable=consider-using-in
return HttpResponseBadRequest(
f"Rendering of the xblock view '{bleach.clean(requested_view, strip=True)}' is not supported."
)

staff_access = has_access(request.user, 'staff', course_key)

with modulestore().bulk_operations(course_key):
# verify the user has access to the course, including enrollment check
try:
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=check_if_enrolled)
except CourseAccessRedirect:
raise Http404("Course not found.") # lint-amnesty, pylint: disable=raise-missing-from

# with course access now verified:
# assume masquerading role, if applicable.
# (if we did this *before* the course access check, then course staff
# masquerading as learners would often be denied access, since course
# staff are generally not enrolled, and viewing a course generally
# requires enrollment.)
_course_masquerade, request.user = setup_masquerade(
request,
course_key,
staff_access,
)

# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(
request.user, usage_key.course_key, request=request, only_if_mobile_app=True
)

# get the block, which verifies whether the user has access to the block.
recheck_access = request.GET.get('recheck_access') == '1'
block, _ = get_block_by_usage_id(
request,
str(course_key),
str(usage_key),
disable_staff_debug_info=disable_staff_debug_info,
course=course,
will_recheck_access=recheck_access,
)

student_view_context = request.GET.dict()
student_view_context['show_bookmark_button'] = request.GET.get('show_bookmark_button', '0') == '1'
student_view_context['show_title'] = request.GET.get('show_title', '1') == '1'

is_learning_mfe = is_request_from_learning_mfe(request)
# Right now, we only care about this in regards to the Learning MFE because it results
# in a bad UX if we display blocks with access errors (repeated upgrade messaging).
# If other use cases appear, consider removing the is_learning_mfe check or switching this
# to be its own query parameter that can toggle the behavior.
student_view_context['hide_access_error_blocks'] = is_learning_mfe and recheck_access
is_mobile_app = is_request_from_mobile_app(request)
student_view_context['is_mobile_app'] = is_mobile_app

enable_completion_on_view_service = False
completion_service = block.runtime.service(block, 'completion')
if completion_service and completion_service.completion_tracking_enabled():
if completion_service.blocks_to_mark_complete_on_view({block}):
enable_completion_on_view_service = True
student_view_context['wrap_xblock_data'] = {
'mark-completed-on-view-after-delay': completion_service.get_complete_on_view_delay_ms()
}

missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)

# Some content gating happens only at the Sequence level (e.g. "has this
# timed exam started?").
ancestor_sequence_block = enclosing_sequence_for_gating_checks(block)
if ancestor_sequence_block:
context = {'specific_masquerade': is_masquerading_as_specific_student(request.user, course_key)}
# If the SequenceModule feels that gating is necessary, redirect
# there so we can have some kind of error message at any rate.
if ancestor_sequence_block.descendants_are_gated(context):
return redirect(
reverse(
'render_xblock',
kwargs={'usage_key_string': str(ancestor_sequence_block.location)}
)
)

# For courses using an LTI provider managed by edx-exams:
# Access to exam content is determined by edx-exams and passed to the LMS using a
# JWT url param. There is no longer a need for exam gating or logic inside the
# sequence block or its render call. descendants_are_gated shoule not return true
# for these timed exams. Instead, sequences are assumed gated by default and we look for
# an access token on the request to allow rendering to continue.
if course.proctoring_provider == 'lti_external':
seq_block = ancestor_sequence_block if ancestor_sequence_block else block
if getattr(seq_block, 'is_time_limited', None):
if not _check_sequence_exam_access(request, seq_block.location):
return HttpResponseForbidden("Access to exam content is restricted")

fragment = block.render(requested_view, context=student_view_context)
optimization_flags = get_optimization_flags_for_content(block, fragment)

context = {
'fragment': fragment,
'course': course,
'block': block,
'disable_accordion': True,
'allow_iframing': True,
'disable_header': True,
'disable_footer': True,
'disable_window_wrap': True,
'enable_completion_on_view_service': enable_completion_on_view_service,
'edx_notes_enabled': is_feature_enabled(course, request.user),
'staff_access': staff_access,
'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
'missed_deadlines': missed_deadlines,
'missed_gated_content': missed_gated_content,
'has_ended': course.has_ended(),
'web_app_course_url': get_learning_mfe_home_url(course_key=course.id, url_fragment='home'),
'on_courseware_page': True,
'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
'is_learning_mfe': is_learning_mfe,
'is_mobile_app': is_mobile_app,
'render_course_wide_assets': True,

**optimization_flags,
}
return render_to_response('courseware/courseware-chromeless.html', context)
return HttpResponse(html_data)


def get_optimization_flags_for_content(block, fragment):
23 changes: 23 additions & 0 deletions openedx/features/offline_mode/toggles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""
Feature toggles for the offline mode app.
"""
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag

WAFFLE_FLAG_NAMESPACE = 'offline_mode'

# .. toggle_name: e
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This feature toggle enables the offline mode course
# content generation for mobile devices.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open edX mobile applications

# .. toggle_use_cases: opt_out, open_edx
# .. toggle_creation_date: 2024-06-06
# .. toggle_target_removal_date: None
ENABLE_OFFLINE_MODE = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.enable_offline_mode', __name__)


def is_offline_mode_enabled(course_key=None):
"""
Returns True if the offline mode is enabled for the course, False otherwise.
"""
return ENABLE_OFFLINE_MODE.is_enabled(course_key)