From 138941ec03bd70dadcbb0fed71e0d77b9a218efe Mon Sep 17 00:00:00 2001 From: Matthew Piatetsky Date: Fri, 28 May 2021 15:50:29 -0400 Subject: [PATCH 1/2] feat: add debugging logs to help figure out why experiment isn't activating in production --- openedx/core/djangoapps/courseware_api/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openedx/core/djangoapps/courseware_api/utils.py b/openedx/core/djangoapps/courseware_api/utils.py index 0ecbcf57e6..01359c7a34 100644 --- a/openedx/core/djangoapps/courseware_api/utils.py +++ b/openedx/core/djangoapps/courseware_api/utils.py @@ -11,7 +11,6 @@ from lms.djangoapps.experiments.utils import STREAK_DISCOUNT_EXPERIMENT_FLAG from openedx.features.course_duration_limits.access import get_user_course_expiration_date from openedx.features.discounts.applicability import can_show_streak_discount_experiment_coupon - def get_celebrations_dict(user, enrollment, course, browser_timezone): """ Returns a dict of celebrations that should be performed. From 6922f75a524bb82561122ba7d59735e1f28f4e60 Mon Sep 17 00:00:00 2001 From: Matthew Piatetsky Date: Fri, 28 May 2021 15:50:29 -0400 Subject: [PATCH 2/2] feat: add user id parameter to progress page --- .../progress/v1/serializers.py | 1 + .../progress/v1/tests/test_views.py | 25 ++++++++ .../course_home_api/progress/v1/views.py | 61 ++++++++++++++----- lms/djangoapps/course_home_api/urls.py | 7 +++ .../core/djangoapps/courseware_api/utils.py | 1 + 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/lms/djangoapps/course_home_api/progress/v1/serializers.py b/lms/djangoapps/course_home_api/progress/v1/serializers.py index 41fa0d30bf..12f9a947d5 100644 --- a/lms/djangoapps/course_home_api/progress/v1/serializers.py +++ b/lms/djangoapps/course_home_api/progress/v1/serializers.py @@ -95,6 +95,7 @@ class ProgressTabSerializer(VerifiedModeSerializerMixin): """ Serializer for progress tab """ + username = serializers.CharField() certificate_data = CertificateDataSerializer() completion_summary = serializers.DictField() course_grade = CourseGradeSerializer() diff --git a/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py index 28f314783c..33145249a7 100644 --- a/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py +++ b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py @@ -183,3 +183,28 @@ class ProgressTabTestViews(BaseCourseHomeTests): assert ungraded_score['learner_has_access'] assert not gated_score['learner_has_access'] assert ungated_score['learner_has_access'] + + @override_waffle_flag(COURSE_HOME_MICROFRONTEND_PROGRESS_TAB, active=True) + def test_view_other_students_progress_page(self): + # Test the ability to view progress pages of other students by changing the url + CourseEnrollment.enroll(self.user, self.course.id) + response = self.client.get(self.url) + assert response.data['username'] == self.user.username + + other_user = UserFactory() + self.url = reverse('course-home-progress-tab-other-student', args=[self.course.id, other_user.id]) + CourseEnrollment.enroll(other_user, self.course.id) + + # users with the ccx coach role can view other students' progress pages + with patch( + 'lms.djangoapps.course_home_api.progress.v1.views.has_ccx_coach_role', + return_value=True + ): + response = self.client.get(self.url) + assert response.data['username'] == other_user.username + + # staff users can view other students' progress pages + self.switch_to_staff() + + response = self.client.get(self.url) + assert response.data['username'] == other_user.username diff --git a/lms/djangoapps/course_home_api/progress/v1/views.py b/lms/djangoapps/course_home_api/progress/v1/views.py index 46da3e9720..4b99578f25 100644 --- a/lms/djangoapps/course_home_api/progress/v1/views.py +++ b/lms/djangoapps/course_home_api/progress/v1/views.py @@ -3,6 +3,7 @@ Progress Tab Views """ from django.http.response import Http404 +from django.contrib.auth.models import User from edx_django_utils import monitoring as monitoring_utils from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser @@ -15,10 +16,11 @@ from xmodule.modulestore.django import modulestore from common.djangoapps.student.models import CourseEnrollment from lms.djangoapps.course_home_api.progress.v1.serializers import ProgressTabSerializer from lms.djangoapps.course_home_api.toggles import course_home_mfe_progress_tab_is_active -from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.access import has_access, has_ccx_coach_role from lms.djangoapps.course_blocks.api import get_course_blocks from lms.djangoapps.course_blocks.transformers import start_date +from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException from lms.djangoapps.courseware.courses import get_course_blocks_completion_summary, get_course_with_access, get_studio_url from lms.djangoapps.courseware.masquerade import setup_masquerade from lms.djangoapps.courseware.views.views import get_cert_data @@ -30,6 +32,7 @@ from openedx.core.djangoapps.content.block_structure.api import get_block_struct from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.features.content_type_gating.block_transformers import ContentTypeGateTransformer +from openedx.features.enterprise_support.utils import get_enterprise_learner_generic_name class ProgressTabView(RetrieveAPIView): @@ -41,6 +44,7 @@ class ProgressTabView(RetrieveAPIView): **Example Requests** GET api/course_home/v1/progress/{course_key} + GET api/course_home/v1/progress/{course_key}/{student_id}/ **Response Values** @@ -48,6 +52,7 @@ class ProgressTabView(RetrieveAPIView): end: (date) end date of the course user_has_passing_grade: (bool) boolean on if the user has a passing grade in the course + username: (str) username of the student whose progress information is being displayed. has_scheduled_content: (bool) boolean on if the course has content scheduled with a release date in the future certificate_data: Object containing information about the user's certificate status cert_status: (str) the status of a user's certificate (full list of statuses are defined in @@ -113,6 +118,12 @@ class ProgressTabView(RetrieveAPIView): def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) + student_id = kwargs.get('student_id') + if student_id: + try: + student_id = int(student_id) + except ValueError: + raise Http404 if not course_home_mfe_progress_tab_is_active(course_key): raise Http404 @@ -123,17 +134,36 @@ class ProgressTabView(RetrieveAPIView): monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) is_staff = bool(has_access(request.user, 'staff', course_key)) - _, request.user = setup_masquerade( - request, - course_key, - staff_access=is_staff, - reset_masquerade_data=True - ) + if student_id is None or student_id == request.user.id: + _, student = setup_masquerade( + request, + course_key, + staff_access=is_staff, + reset_masquerade_data=True + ) + else: + # When a student_id is passed in, we display the progress page for the user + # with the provided user id, rather than the requesting user + try: + coach_access = has_ccx_coach_role(request.user, course_key) + except CCXLocatorValidationException: + coach_access = False - course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) + has_access_on_students_profiles = is_staff or coach_access + # Requesting access to a different student's profile + if not has_access_on_students_profiles: + raise Http404 + try: + student = User.objects.get(id=student_id) + except User.DoesNotExist as exc: + raise Http404 from exc + + username = get_enterprise_learner_generic_name(request) or student.username + + course = get_course_with_access(student, 'load', course_key, check_if_enrolled=False) course_overview = CourseOverview.get_from_id(course_key) - enrollment = CourseEnrollment.get_enrollment(request.user, course_key) + enrollment = CourseEnrollment.get_enrollment(student, course_key) enrollment_mode = getattr(enrollment, 'mode', None) if not (enrollment and enrollment.is_active) and not is_staff: @@ -142,14 +172,14 @@ class ProgressTabView(RetrieveAPIView): # The block structure is used for both the course_grade and has_scheduled content fields # So it is called upfront and reused for optimization purposes collected_block_structure = get_block_structure_manager(course_key).get_collected() - course_grade = CourseGradeFactory().read(request.user, collected_block_structure=collected_block_structure) + course_grade = CourseGradeFactory().read(student, collected_block_structure=collected_block_structure) # Get has_scheduled_content data transformers = BlockStructureTransformers() transformers += [start_date.StartDateTransformer(), ContentTypeGateTransformer()] usage_key = collected_block_structure.root_block_usage_key course_blocks = get_course_blocks( - request.user, + student, usage_key, transformers=transformers, collected_block_structure=collected_block_structure, @@ -159,13 +189,13 @@ class ProgressTabView(RetrieveAPIView): # Get user_has_passing_grade data user_has_passing_grade = False - if not request.user.is_anonymous: + if not student.is_anonymous: user_grade = course_grade.percent user_has_passing_grade = user_grade >= course.lowest_passing_grade descriptor = modulestore().get_course(course_key) grading_policy = descriptor.grading_policy - verification_status = IDVerificationService.user_status(request.user) + verification_status = IDVerificationService.user_status(student) verification_link = None if verification_status['status'] is None or verification_status['status'] == 'expired': verification_link = IDVerificationService.get_verify_location(course_id=course_key) @@ -178,10 +208,11 @@ class ProgressTabView(RetrieveAPIView): } data = { + 'username': username, 'end': course.end, 'user_has_passing_grade': user_has_passing_grade, - 'certificate_data': get_cert_data(request.user, course, enrollment_mode, course_grade), - 'completion_summary': get_course_blocks_completion_summary(course_key, request.user), + 'certificate_data': get_cert_data(student, course, enrollment_mode, course_grade), + 'completion_summary': get_course_blocks_completion_summary(course_key, student), 'course_grade': course_grade, 'has_scheduled_content': has_scheduled_content, 'section_scores': course_grade.chapter_grades.values(), diff --git a/lms/djangoapps/course_home_api/urls.py b/lms/djangoapps/course_home_api/urls.py index e15585e30c..671a90622f 100644 --- a/lms/djangoapps/course_home_api/urls.py +++ b/lms/djangoapps/course_home_api/urls.py @@ -57,6 +57,13 @@ urlpatterns += [ ] # Progress Tab URLs +urlpatterns += [ + re_path( + fr'v1/progress/{settings.COURSE_KEY_PATTERN}/(?P[^/]+)', + ProgressTabView.as_view(), + name='course-home-progress-tab-other-student' + ), +] urlpatterns += [ re_path( fr'v1/progress/{settings.COURSE_KEY_PATTERN}', diff --git a/openedx/core/djangoapps/courseware_api/utils.py b/openedx/core/djangoapps/courseware_api/utils.py index 01359c7a34..0ecbcf57e6 100644 --- a/openedx/core/djangoapps/courseware_api/utils.py +++ b/openedx/core/djangoapps/courseware_api/utils.py @@ -11,6 +11,7 @@ from lms.djangoapps.experiments.utils import STREAK_DISCOUNT_EXPERIMENT_FLAG from openedx.features.course_duration_limits.access import get_user_course_expiration_date from openedx.features.discounts.applicability import can_show_streak_discount_experiment_coupon + def get_celebrations_dict(user, enrollment, course, browser_timezone): """ Returns a dict of celebrations that should be performed.