From 99163bdf2caed0e22005c96cbae2698d694c55c3 Mon Sep 17 00:00:00 2001 From: Carla Duarte <cduarte@edx.org> Date: Fri, 20 Nov 2020 16:00:27 -0500 Subject: [PATCH] AA-131: Allow anonymous users through course home MFE --- common/lib/xmodule/xmodule/seq_module.py | 4 +- .../outline/v1/tests/test_views.py | 13 +- .../course_home_api/outline/v1/views.py | 128 +++++++++--------- lms/djangoapps/courseware/courses.py | 2 + lms/djangoapps/courseware/views/views.py | 15 +- lms/djangoapps/experiments/flags.py | 21 ++- .../experiments/stable_bucketing.py | 9 +- lms/djangoapps/experiments/views_custom.py | 2 +- .../core/djangoapps/courseware_api/views.py | 8 +- .../course_experience/views/course_dates.py | 1 - .../course_experience/views/course_home.py | 8 +- openedx/features/discounts/applicability.py | 2 +- 12 files changed, 122 insertions(+), 91 deletions(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 8a165e8be4c..2f23b2cd9e0 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -224,7 +224,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): progress = reduce(Progress.add_counts, progresses, None) return progress - def handle_ajax(self, dispatch, data): # TODO: bounds checking + def handle_ajax(self, dispatch, data, view=STUDENT_VIEW): # TODO: bounds checking ''' get = request.POST instance ''' if dispatch == 'goto_position': # set position to default value if either 'position' argument not @@ -263,7 +263,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): else: # check if prerequisite has been met prereq_met, prereq_meta_info = self._compute_is_prereq_met(True) - meta = self._get_render_metadata(context, display_items, prereq_met, prereq_meta_info, banner_text, STUDENT_VIEW) + meta = self._get_render_metadata(context, display_items, prereq_met, prereq_meta_info, banner_text, view) meta['display_name'] = self.display_name_with_default meta['format'] = getattr(self, 'format', '') return json.dumps(meta) diff --git a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py index 7a6a8975b75..111aad28178 100644 --- a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py +++ b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py @@ -96,7 +96,18 @@ class OutlineTabTestViews(BaseCourseHomeTests): def test_get_unauthenticated_user(self): self.client.logout() response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 200) + + course_blocks = response.data.get('course_blocks') + self.assertEqual(course_blocks, None) + + course_tools = response.data.get('course_tools') + self.assertEqual(len(course_tools), 0) + + dates_widget = response.data.get('dates_widget') + self.assertTrue(dates_widget) + date_blocks = dates_widget.get('course_date_blocks') + self.assertEqual(len(date_blocks), 0) @override_experiment_waffle_flag(COURSE_HOME_MICROFRONTEND, active=True) @override_waffle_flag(COURSE_HOME_MICROFRONTEND_OUTLINE_TAB, active=True) diff --git a/lms/djangoapps/course_home_api/outline/v1/views.py b/lms/djangoapps/course_home_api/outline/v1/views.py index 15d2298f031..93c111b0a55 100644 --- a/lms/djangoapps/course_home_api/outline/v1/views.py +++ b/lms/djangoapps/course_home_api/outline/v1/views.py @@ -134,12 +134,10 @@ class OutlineTabView(RetrieveAPIView): **Returns** * 200 on success with above fields. - * 403 if the user is not authenticated. * 404 if the course is not available or cannot be seen. """ - permission_classes = (IsAuthenticated,) serializer_class = OutlineTabSerializer def get(self, request, *args, **kwargs): @@ -169,32 +167,6 @@ class OutlineTabView(RetrieveAPIView): allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course_key) allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC allow_public_outline = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE - is_enrolled = enrollment and enrollment.is_active - is_staff = bool(has_access(request.user, 'staff', course_key)) - show_enrolled = is_enrolled or is_staff - - show_handouts = show_enrolled or allow_public - handouts_html = get_course_info_section(request, request.user, course, 'handouts') if show_handouts else '' - - offer_data = show_enrolled and generate_offer_data(request.user, course_overview) - access_expiration = show_enrolled and get_access_expiration_data(request.user, course_overview) - - welcome_message_html = show_enrolled and get_current_update_for_user(request, course) - - enroll_alert = { - 'can_enroll': True, - 'extra_text': None, - } - if not show_enrolled: - if CourseMode.is_masters_only(course_key): - enroll_alert['can_enroll'] = False - enroll_alert['extra_text'] = _('Please contact your degree administrator or ' - 'edX Support if you have questions.') - elif course.invitation_only: - enroll_alert['can_enroll'] = False - - course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key) - date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) @@ -204,16 +176,62 @@ class OutlineTabView(RetrieveAPIView): if course_home_mfe_dates_tab_is_active(course.id): dates_tab_link = get_microfrontend_url(course_key=course.id, view_name='dates') + # Set all of the defaults + access_expiration = None course_blocks = None - if show_enrolled or allow_public or allow_public_outline: - outline_user = request.user if show_enrolled else None - course_blocks = get_course_outline_block_tree(request, course_key_string, outline_user) - + course_goals = { + 'goal_options': [], + 'selected_goal': None + } + course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key) + dates_widget = { + 'course_date_blocks': [], + 'dates_tab_link': dates_tab_link, + 'user_timezone': user_timezone, + } + enroll_alert = { + 'can_enroll': True, + 'extra_text': None, + } + handouts_html = None + offer_data = None resume_course = { 'has_visited_course': False, 'url': None, } + welcome_message_html = None + + is_enrolled = enrollment and enrollment.is_active + is_staff = bool(has_access(request.user, 'staff', course_key)) + show_enrolled = is_enrolled or is_staff if show_enrolled: + course_blocks = get_course_outline_block_tree(request, course_key_string, request.user) + date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) + dates_widget['course_date_blocks'] = [block for block in date_blocks if not isinstance(block, TodaysDate)] + + handouts_html = get_course_info_section(request, request.user, course, 'handouts') + welcome_message_html = get_current_update_for_user(request, course) + + offer_data = generate_offer_data(request.user, course_overview) + access_expiration = get_access_expiration_data(request.user, course_overview) + + # Only show the set course goal message for enrolled, unverified + # users in a course that allows for verified statuses. + is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key) + if not is_already_verified and has_course_goal_permission(request, course_key_string, + {'is_enrolled': is_enrolled}): + course_goals = { + 'goal_options': valid_course_goals_ordered(include_unsure=True), + 'selected_goal': None + } + + selected_goal = get_course_goal(request.user, course_key) + if selected_goal: + course_goals['selected_goal'] = { + 'key': selected_goal.goal_key, + 'text': get_course_goal_text(selected_goal.goal_key), + } + try: resume_block = get_key_to_last_completed_block(request.user, course.id) resume_course['has_visited_course'] = True @@ -224,47 +242,31 @@ class OutlineTabView(RetrieveAPIView): 'location': str(resume_block) }) resume_course['url'] = request.build_absolute_uri(resume_path) + elif allow_public_outline or allow_public: + course_blocks = get_course_outline_block_tree(request, course_key_string, None) + if allow_public: + handouts_html = get_course_info_section(request, request.user, course, 'handouts') - dates_widget = { - 'course_date_blocks': [block for block in date_blocks if not isinstance(block, TodaysDate)], - 'dates_tab_link': dates_tab_link, - 'user_timezone': user_timezone, - } - - # Only show the set course goal message for enrolled, unverified - # users in a course that allows for verified statuses. - is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key) - if (not is_already_verified and - has_course_goal_permission(request, course_key_string, {'is_enrolled': is_enrolled})): - course_goals = { - 'goal_options': valid_course_goals_ordered(include_unsure=True), - 'selected_goal': None - } - - selected_goal = get_course_goal(request.user, course_key) - if selected_goal: - course_goals['selected_goal'] = { - 'key': selected_goal.goal_key, - 'text': get_course_goal_text(selected_goal.goal_key), - } - else: - course_goals = { - 'goal_options': [], - 'selected_goal': None - } + if not show_enrolled: + if CourseMode.is_masters_only(course_key): + enroll_alert['can_enroll'] = False + enroll_alert['extra_text'] = _('Please contact your degree administrator or ' + 'edX Support if you have questions.') + elif course.invitation_only: + enroll_alert['can_enroll'] = False data = { - 'access_expiration': access_expiration or None, + 'access_expiration': access_expiration, 'course_blocks': course_blocks, 'course_goals': course_goals, 'course_tools': course_tools, 'dates_widget': dates_widget, 'enroll_alert': enroll_alert, - 'handouts_html': handouts_html or None, + 'handouts_html': handouts_html, 'has_ended': course.has_ended(), - 'offer': offer_data or None, + 'offer': offer_data, 'resume_course': resume_course, - 'welcome_message_html': welcome_message_html or None, + 'welcome_message_html': welcome_message_html, } context = self.get_serializer_context() context['course_overview'] = course_overview diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index a63a479e50e..19eef9f3dfd 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -525,6 +525,8 @@ def get_course_assignments(course_key, user, include_access=False): Each returned object is a namedtuple with fields: title, url, date, contains_gated_content, complete, past_due, assignment_type """ + if not user.id: + return [] store = modulestore() course_usage_key = store.make_course_usage_key(course_key) block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 582438de501..fe7899187c1 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -55,7 +55,8 @@ from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import CertificateStatuses from lms.djangoapps.commerce.utils import EcommerceService -from lms.djangoapps.course_home_api.utils import is_request_from_learning_mfe +from lms.djangoapps.course_home_api.toggles import course_home_mfe_dates_tab_is_active +from lms.djangoapps.course_home_api.utils import get_microfrontend_url, is_request_from_learning_mfe from lms.djangoapps.courseware.access import has_access, has_ccx_coach_role from lms.djangoapps.courseware.access_utils import check_course_open_for_learner, check_public_access from lms.djangoapps.courseware.courses import ( @@ -643,7 +644,8 @@ class CourseTabView(EdxFragmentView): register_label=_("register"), current_url=urlquote_plus(request.path), ), - ) + ), + once_only=True ) else: PageLevelMessages.register_warning_message( @@ -1010,6 +1012,9 @@ def dates(request, course_id): from lms.urls import COURSE_DATES_NAME, RESET_COURSE_DEADLINES_NAME course_key = CourseKey.from_string(course_id) + if course_home_mfe_dates_tab_is_active(course_key) and not request.user.is_staff: + microfrontend_url = get_microfrontend_url(course_key=course_key, view_name=COURSE_DATES_NAME) + raise Redirect(microfrontend_url) # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', text_type(course_key)) @@ -1617,7 +1622,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): 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). """ - from lms.urls import COURSE_DATES_NAME, RESET_COURSE_DEADLINES_NAME + from lms.urls import RESET_COURSE_DEADLINES_NAME from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME usage_key = UsageKey.from_string(usage_key_string) @@ -1626,7 +1631,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): course_key = usage_key.course_key requested_view = request.GET.get('view', 'student_view') - if requested_view != 'student_view': + if requested_view != 'student_view' and requested_view != 'public_view': return HttpResponseBadRequest( u"Rendering of the xblock view '{}' is not supported.".format(bleach.clean(requested_view, strip=True)) ) @@ -1659,7 +1664,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user) context = { - 'fragment': block.render('student_view', context=student_view_context), + 'fragment': block.render(requested_view, context=student_view_context), 'course': course, 'disable_accordion': True, 'allow_iframing': True, diff --git a/lms/djangoapps/experiments/flags.py b/lms/djangoapps/experiments/flags.py index 4d9fd09c67c..9af33fd93fc 100644 --- a/lms/djangoapps/experiments/flags.py +++ b/lms/djangoapps/experiments/flags.py @@ -189,16 +189,14 @@ class ExperimentWaffleFlag(CourseWaffleFlag): if not request: return 0 - if not hasattr(request, 'user') or not request.user.id: - # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users - return 0 + if hasattr(request, 'user'): + user = get_specific_masquerading_user(request.user, course_key) - user = get_specific_masquerading_user(request.user, course_key) - if user is None: - user = request.user - masquerading_as_specific_student = False - else: - masquerading_as_specific_student = True + if user is None: + user = request.user + masquerading_as_specific_student = False + else: + masquerading_as_specific_student = True # If a course key is passed in, include it in the experiment name # in order to separate caches and analytics calls per course-run. @@ -238,14 +236,15 @@ class ExperimentWaffleFlag(CourseWaffleFlag): break else: bucket = stable_bucketing_hash_group( - bucketing_group_name, self.num_buckets, user.username + bucketing_group_name, self.num_buckets, user ) session_key = 'tracked.{}'.format(experiment_name) + anonymous = not hasattr(request, 'user') or not request.user.id if ( track and hasattr(request, 'session') and session_key not in request.session and - not masquerading_as_specific_student + not masquerading_as_specific_student and not anonymous ): segment.track( user_id=user.id, diff --git a/lms/djangoapps/experiments/stable_bucketing.py b/lms/djangoapps/experiments/stable_bucketing.py index b6709fd9639..bc3039ac32c 100644 --- a/lms/djangoapps/experiments/stable_bucketing.py +++ b/lms/djangoapps/experiments/stable_bucketing.py @@ -12,7 +12,7 @@ import hashlib import re -def stable_bucketing_hash_group(group_name, group_count, username): +def stable_bucketing_hash_group(group_name, group_count, user): """ Return the bucket that a user should be in for a given stable bucketing assignment. @@ -22,11 +22,14 @@ def stable_bucketing_hash_group(group_name, group_count, username): Arguments: group_name: The name of the grouping/experiment. group_count: How many groups to bucket users into. - username: The username of the user being bucketed. + user: The user being bucketed. """ + # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users + if not user or not user.id: + return 0 hasher = hashlib.md5() hasher.update(group_name.encode('utf-8')) - hasher.update(username.encode('utf-8')) + hasher.update(user.username.encode('utf-8')) hash_str = hasher.hexdigest() return int(re.sub('[8-9a-f]', '1', re.sub('[0-7]', '0', hash_str)), 2) % group_count diff --git a/lms/djangoapps/experiments/views_custom.py b/lms/djangoapps/experiments/views_custom.py index a1e901cd248..0cbcfe2eb8a 100644 --- a/lms/djangoapps/experiments/views_custom.py +++ b/lms/djangoapps/experiments/views_custom.py @@ -138,7 +138,7 @@ class Rev934(DeveloperErrorViewMixin, APIView): upgrade_price = six.text_type(get_cosmetic_verified_display_price(course)) could_upsell = bool(user_upsell and basket_url) - bucket = stable_bucketing_hash_group(MOBILE_UPSELL_EXPERIMENT, 2, user.username) + bucket = stable_bucketing_hash_group(MOBILE_UPSELL_EXPERIMENT, 2, user) if could_upsell and hasattr(request, 'session') and MOBILE_UPSELL_EXPERIMENT not in request.session: properties = { diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index 5e12e1f9c61..46c4f32ce95 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -47,6 +47,7 @@ from common.djangoapps.student.models import ( ) from xmodule.modulestore.django import modulestore from xmodule.modulestore.search import path_to_location +from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW from .serializers import CourseInfoSerializer from .utils import serialize_upgrade_info @@ -483,7 +484,12 @@ class SequenceMetadata(DeveloperErrorViewMixin, APIView): str(usage_key.course_key), str(usage_key), disable_staff_debug_info=True) - return Response(json.loads(sequence.handle_ajax('metadata', None))) + + view = STUDENT_VIEW + if request.user.is_anonymous: + view = PUBLIC_VIEW + + return Response(json.loads(sequence.handle_ajax('metadata', None, view=view))) class Resume(DeveloperErrorViewMixin, APIView): diff --git a/openedx/features/course_experience/views/course_dates.py b/openedx/features/course_experience/views/course_dates.py index 78e439742bc..b2e8ae0b744 100644 --- a/openedx/features/course_experience/views/course_dates.py +++ b/openedx/features/course_experience/views/course_dates.py @@ -12,7 +12,6 @@ from django.utils.translation import get_language_bidi from opaque_keys.edx.keys import CourseKey from web_fragments.fragment import Fragment -from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access from lms.djangoapps.courseware.tabs import DatesTab from lms.djangoapps.course_home_api.toggles import course_home_mfe_dates_tab_is_active diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py index be35a428c24..80d6686b475 100644 --- a/openedx/features/course_experience/views/course_home.py +++ b/openedx/features/course_experience/views/course_home.py @@ -14,16 +14,17 @@ from django.views.decorators.csrf import ensure_csrf_cookie from opaque_keys.edx.keys import CourseKey from web_fragments.fragment import Fragment +from lms.djangoapps.course_home_api.toggles import course_home_mfe_outline_tab_is_active +from lms.djangoapps.course_home_api.utils import get_microfrontend_url from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.courses import can_self_enroll_in_course, get_course_info_section, get_course_with_access -from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.course_goals.api import ( get_course_goal, get_course_goal_options, get_goal_api_url, has_course_goal_permission ) -from lms.djangoapps.courseware.exceptions import CourseAccessRedirect +from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect from lms.djangoapps.courseware.utils import can_show_verified_upgrade, verified_upgrade_deadline_link from lms.djangoapps.courseware.views.views import CourseTabView from lms.djangoapps.courseware.toggles import COURSEWARE_PROCTORING_IMPROVEMENTS @@ -70,6 +71,9 @@ class CourseHomeView(CourseTabView): def render_to_fragment(self, request, course=None, tab=None, **kwargs): course_id = six.text_type(course.id) + if course_home_mfe_outline_tab_is_active(course.id) and not request.user.is_staff: + microfrontend_url = get_microfrontend_url(course_key=course_id, view_name="home") + raise Redirect(microfrontend_url) home_fragment_view = CourseHomeFragmentView() return home_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs) diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py index d0efa0ac49a..f979e46eeb8 100644 --- a/openedx/features/discounts/applicability.py +++ b/openedx/features/discounts/applicability.py @@ -150,7 +150,7 @@ def _is_in_holdback_and_bucket(user): return False # Holdback is 10% - bucket = stable_bucketing_hash_group(DISCOUNT_APPLICABILITY_HOLDBACK, 10, user.username) + bucket = stable_bucketing_hash_group(DISCOUNT_APPLICABILITY_HOLDBACK, 10, user) request = get_current_request() if hasattr(request, 'session') and DISCOUNT_APPLICABILITY_HOLDBACK not in request.session: -- GitLab