diff --git a/lms/djangoapps/course_home_api/outline/views.py b/lms/djangoapps/course_home_api/outline/views.py index 7c5307cba764..f34d9cd402e3 100644 --- a/lms/djangoapps/course_home_api/outline/views.py +++ b/lms/djangoapps/course_home_api/outline/views.py @@ -602,6 +602,8 @@ def completions_dict(self): Dictionary keys are block keys and values are int values representing the completion status of the block. """ + if self.request.user.is_anonymous: + return {} course_key_string = self.kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) completions = BlockCompletion.objects.filter(user=self.request.user, context_key=course_key).values_list( diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index a1a46137fcca..a43b29046880 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -32,6 +32,7 @@ ACCESS_DENIED, ACCESS_GRANTED, check_course_open_for_learner, + check_public_access, check_start_date, debug, ) @@ -41,6 +42,7 @@ from lms.djangoapps.mobile_api.models import IgnoreMobileAvailableFlagConfig from lms.djangoapps.courseware.toggles import course_is_invitation_only from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.djangoapps.content.learning_sequences.models import CourseContext, LearningContext from openedx.features.course_duration_limits.access import check_course_expired from common.djangoapps.student import auth from common.djangoapps.student.models import CourseEnrollmentAllowed @@ -61,7 +63,12 @@ get_pre_requisite_courses_not_completed, is_prerequisite_courses_enabled ) -from xmodule.course_block import CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_CATALOG_AND_ABOUT, CourseBlock # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.course_block import ( # lint-amnesty, pylint: disable=wrong-import-order + CATALOG_VISIBILITY_ABOUT, + CATALOG_VISIBILITY_CATALOG_AND_ABOUT, + COURSE_VISIBILITY_PUBLIC, + CourseBlock, +) from xmodule.error_block import ErrorBlock # lint-amnesty, pylint: disable=wrong-import-order from xmodule.partitions.partitions import NoSuchUserPartitionError, NoSuchUserPartitionGroupError # lint-amnesty, pylint: disable=wrong-import-order @@ -364,6 +371,10 @@ def can_load(): else: return visible_to_nonstaff + # Anonymous users on public courses can load (so course_metadata returns has_access and MFE doesn't redirect). + if not user.is_authenticated and check_public_access(courselike, [COURSE_VISIBILITY_PUBLIC]): + return ACCESS_GRANTED + open_for_learner = check_course_open_for_learner(user, courselike) if not open_for_learner: staff_access = _has_staff_access_to_block(user, courselike, courselike.id) @@ -581,6 +592,19 @@ def can_load(): students to see blocks. If not, views should check the course, so we don't have to hit the enrollments table on every block load. """ + # If the user has staff access, they can load the block and checks below are not needed. + staff_access_response = _has_staff_access_to_block(user, block, course_key) + if staff_access_response: + return staff_access_response + + # Anonymous users: allow block load when course has public (unenrolled) access, + # so that Learning MFE and sequence API can serve public_view without requiring login. + if not user.is_authenticated and course_key: + course_like = _get_course_like_for_public_access(course_key) + if course_like and check_public_access(course_like, [COURSE_VISIBILITY_PUBLIC]): + # Only visibility check; skip start date so sequence metadata matches outline API. + return _visible_to_nonstaff_users(block, display_error_to_user=False) + # If the user (or the role the user is currently masquerading as) does not have # access to this content, then deny access. The problem with calling _has_staff_access_to_block # before this method is that _has_staff_access_to_block short-circuits and returns True @@ -589,11 +613,6 @@ def can_load(): if not group_access_response: return group_access_response - # If the user has staff access, they can load the block and checks below are not needed. - staff_access_response = _has_staff_access_to_block(user, block, course_key) - if staff_access_response: - return staff_access_response - return ( _visible_to_nonstaff_users(block, display_error_to_user=False) and ( @@ -693,6 +712,36 @@ def check_support(): ##### Internal helper methods below +def _get_course_like_for_public_access(course_key): + """ + Return an object with .id and .course_visibility for check_public_access. + Prefer learning_sequences.CourseContext (same source as outline API). + """ + try: + course_context = ( + LearningContext.objects + .select_related('course_context') + .get(context_key=course_key) + .course_context + ) + return type('CourseLike', (), { + 'id': course_key, + 'course_visibility': course_context.course_visibility, + })() + except (LearningContext.DoesNotExist, CourseContext.DoesNotExist): + pass + try: + overview = CourseOverview.get_from_id(course_key) + if overview is not None: + return type('CourseLike', (), { + 'id': course_key, + 'course_visibility': overview.course_visibility, + })() + except (CourseOverview.DoesNotExist, IOError): + pass + return None + + def _dispatch(table, action, user, obj): """ Helper: call table[action], raising a nice pretty error if there is no such key. diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index bbf9d5394705..09b1bb361710 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -35,8 +35,14 @@ OldMongoAccessError, StartDateError ) -from lms.djangoapps.courseware.access_utils import check_authentication, check_data_sharing_consent, check_enrollment, \ - check_correct_active_enterprise_customer, is_priority_access_error +from lms.djangoapps.courseware.access_utils import ( + check_authentication, + check_correct_active_enterprise_customer, + check_data_sharing_consent, + check_enrollment, + check_public_access, + is_priority_access_error, +) from lms.djangoapps.courseware.context_processor import get_user_timezone_or_last_seen_timezone_or_utc from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException from lms.djangoapps.courseware.date_summary import ( @@ -66,6 +72,7 @@ from openedx.features.course_duration_limits.access import AuditExpiredError from openedx.features.course_experience import RELATIVE_DATES_FLAG from openedx.features.course_experience.utils import is_block_structure_complete_for_assignments +from xmodule.course_block import COURSE_VISIBILITY_PUBLIC # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order from xmodule.x_module import STUDENT_VIEW # lint-amnesty, pylint: disable=wrong-import-order @@ -246,6 +253,11 @@ def check_course_access_with_redirect( # StartDateError should be ignored if isinstance(access_response, StartDateError) and allow_not_started_courses: return + # Anonymous users on public courses can view before start date (e.g. Learning MFE public_view). + if isinstance(access_response, StartDateError) and not user.is_authenticated and check_public_access( + course, [COURSE_VISIBILITY_PUBLIC] + ): + return # Redirect if StartDateError if isinstance(access_response, StartDateError): start_date = strftime_localized(course.start, 'SHORT_DATE') diff --git a/xmodule/video_block/video_block.py b/xmodule/video_block/video_block.py index 4cf58420b69e..e7e51280ea01 100644 --- a/xmodule/video_block/video_block.py +++ b/xmodule/video_block/video_block.py @@ -265,7 +265,7 @@ def public_view(self, context): fragment = Fragment(self.get_html(view=PUBLIC_VIEW, context=context)) add_css_to_fragment(fragment, 'VideoBlockDisplay.css') - add_webpack_js_to_fragment(fragment, 'VideoBlockMain') + add_webpack_js_to_fragment(fragment, 'VideoBlockDisplay') fragment.initialize_js('Video') return fragment