From a2bd8a7525ec1f1d870f9089291e745ee9997e6c Mon Sep 17 00:00:00 2001 From: Awais Qureshi Date: Mon, 7 Oct 2024 16:09:32 +0500 Subject: [PATCH] upgrade get_students_features api with DRF( 7th api ) (#35323) * feat: upgrading simple api to drf compatible. --- lms/djangoapps/instructor/views/api.py | 181 +++++++++++--------- lms/djangoapps/instructor/views/api_urls.py | 2 +- 2 files changed, 101 insertions(+), 82 deletions(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 98df3e48bcf3..2b1f28e4ceac 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1416,13 +1416,9 @@ def get_issued_certificates(request, course_id): return JsonResponse(response_payload) -@transaction.non_atomic_requests -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.CAN_RESEARCH) -@common_exceptions_400 -def get_students_features(request, course_id, csv=False): # pylint: disable=redefined-outer-name +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +@method_decorator(transaction.non_atomic_requests, name='dispatch') +class GetStudentsFeatures(DeveloperErrorViewMixin, APIView): """ Respond with json which contains a summary of all enrolled students profile information. @@ -1431,86 +1427,108 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red TO DO accept requests for different attribute sets. """ - course_key = CourseKey.from_string(course_id) - course = get_course_by_id(course_key) - report_type = _('enrolled learner profile') - available_features = instructor_analytics_basic.AVAILABLE_FEATURES - - # Allow for sites to be able to define additional columns. - # Note that adding additional columns has the potential to break - # the student profile report due to a character limit on the - # asynchronous job input which in this case is a JSON string - # containing the list of columns to include in the report. - # TODO: Refactor the student profile report code to remove the list of columns - # that should be included in the report from the asynchronous job input. - # We need to clone the list because we modify it below - query_features = list(configuration_helpers.get_value('student_profile_download_fields', [])) - - if not query_features: - query_features = [ - 'id', 'username', 'name', 'email', 'language', 'location', - 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', - 'goals', 'enrollment_mode', 'last_login', 'date_joined', 'external_user_key' - ] - keep_field_private(query_features, 'year_of_birth') # protected information - - # Provide human-friendly and translatable names for these features. These names - # will be displayed in the table generated in data_download.js. It is not (yet) - # used as the header row in the CSV, but could be in the future. - query_features_names = { - 'id': _('User ID'), - 'username': _('Username'), - 'name': _('Name'), - 'email': _('Email'), - 'language': _('Language'), - 'location': _('Location'), - # 'year_of_birth': _('Birth Year'), treated as privileged information as of TNL-10683, not to go in reports - 'gender': _('Gender'), - 'level_of_education': _('Level of Education'), - 'mailing_address': _('Mailing Address'), - 'goals': _('Goals'), - 'enrollment_mode': _('Enrollment Mode'), - 'last_login': _('Last Login'), - 'date_joined': _('Date Joined'), - 'external_user_key': _('External User Key'), - } - - if is_course_cohorted(course.id): - # Translators: 'Cohort' refers to a group of students within a course. - query_features.append('cohort') - query_features_names['cohort'] = _('Cohort') + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.CAN_RESEARCH - if course.teams_enabled: - query_features.append('team') - query_features_names['team'] = _('Team') + @method_decorator(ensure_csrf_cookie) + @method_decorator(transaction.non_atomic_requests) + def post(self, request, course_id, csv=False): # pylint: disable=redefined-outer-name + """ + Handle POST requests to retrieve student profile information for a specific course. - # For compatibility reasons, city and country should always appear last. - query_features.append('city') - query_features_names['city'] = _('City') - query_features.append('country') - query_features_names['country'] = _('Country') + Args: + request: The HTTP request object. + course_id: The ID of the course for which to retrieve student information. + csv: Optional; if 'csv' is present in the URL, it indicates that the response should be in CSV format. + Defaults to None. - if not csv: - student_data = instructor_analytics_basic.enrolled_students_features(course_key, query_features) - response_payload = { - 'course_id': str(course_key), - 'students': student_data, - 'students_count': len(student_data), - 'queried_features': query_features, - 'feature_names': query_features_names, - 'available_features': available_features, + Returns: + Response: A JSON response containing student profile information, or CSV if the `csv` parameter is provided. + """ + course_key = CourseKey.from_string(course_id) + course = get_course_by_id(course_key) + report_type = _('enrolled learner profile') + available_features = instructor_analytics_basic.AVAILABLE_FEATURES + + # Allow for sites to be able to define additional columns. + # Note that adding additional columns has the potential to break + # the student profile report due to a character limit on the + # asynchronous job input which in this case is a JSON string + # containing the list of columns to include in the report. + # TODO: Refactor the student profile report code to remove the list of columns + # that should be included in the report from the asynchronous job input. + # We need to clone the list because we modify it below + query_features = list(configuration_helpers.get_value('student_profile_download_fields', [])) + + if not query_features: + query_features = [ + 'id', 'username', 'name', 'email', 'language', 'location', + 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', + 'goals', 'enrollment_mode', 'last_login', 'date_joined', 'external_user_key' + ] + keep_field_private(query_features, 'year_of_birth') # protected information + + # Provide human-friendly and translatable names for these features. These names + # will be displayed in the table generated in data_download.js. It is not (yet) + # used as the header row in the CSV, but could be in the future. + query_features_names = { + 'id': _('User ID'), + 'username': _('Username'), + 'name': _('Name'), + 'email': _('Email'), + 'language': _('Language'), + 'location': _('Location'), + # 'year_of_birth': _('Birth Year'), treated as privileged information as of TNL-10683, + # not to go in reports + 'gender': _('Gender'), + 'level_of_education': _('Level of Education'), + 'mailing_address': _('Mailing Address'), + 'goals': _('Goals'), + 'enrollment_mode': _('Enrollment Mode'), + 'last_login': _('Last Login'), + 'date_joined': _('Date Joined'), + 'external_user_key': _('External User Key'), } - return JsonResponse(response_payload) - else: - task_api.submit_calculate_students_features_csv( - request, - course_key, - query_features - ) - success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) + if is_course_cohorted(course.id): + # Translators: 'Cohort' refers to a group of students within a course. + query_features.append('cohort') + query_features_names['cohort'] = _('Cohort') - return JsonResponse({"status": success_status}) + if course.teams_enabled: + query_features.append('team') + query_features_names['team'] = _('Team') + + # For compatibility reasons, city and country should always appear last. + query_features.append('city') + query_features_names['city'] = _('City') + query_features.append('country') + query_features_names['country'] = _('Country') + + if not csv: + student_data = instructor_analytics_basic.enrolled_students_features(course_key, query_features) + response_payload = { + 'course_id': str(course_key), + 'students': student_data, + 'students_count': len(student_data), + 'queried_features': query_features, + 'feature_names': query_features_names, + 'available_features': available_features, + } + return JsonResponse(response_payload) + + else: + try: + task_api.submit_calculate_students_features_csv( + request, + course_key, + query_features + ) + success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type) + except Exception as e: + raise self.api_error(status.HTTP_400_BAD_REQUEST, str(e), 'Requested task is already running') + + return JsonResponse({"status": success_status}) @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') @@ -1637,6 +1655,7 @@ def post(self, request, course_key_string): task_api.submit_cohort_students(request, course_key, file_name) except (FileValidationException, ValueError) as e: raise self.api_error(status.HTTP_400_BAD_REQUEST, str(e), 'failed-validation') + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index e7309d7f5f81..eef299d1d7cb 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -27,8 +27,8 @@ path('modify_access', api.ModifyAccess.as_view(), name='modify_access'), path('bulk_beta_modify_access', api.bulk_beta_modify_access, name='bulk_beta_modify_access'), path('get_problem_responses', api.get_problem_responses, name='get_problem_responses'), + re_path(r'^get_students_features(?P/csv)?$', api.GetStudentsFeatures.as_view(), name='get_students_features'), path('get_grading_config', api.GetGradingConfig.as_view(), name='get_grading_config'), - re_path(r'^get_students_features(?P/csv)?$', api.get_students_features, name='get_students_features'), path('get_issued_certificates/', api.get_issued_certificates, name='get_issued_certificates'), path('get_students_who_may_enroll', api.GetStudentsWhoMayEnroll.as_view(), name='get_students_who_may_enroll'), path('get_anon_ids', api.GetAnonIds.as_view(), name='get_anon_ids'),