From e2aa883dcafa4f9231eaae47045813abfe05f629 Mon Sep 17 00:00:00 2001 From: Ahmed Khalid <106074266+ahmed-arb@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:44:16 +0500 Subject: [PATCH 1/9] feat: bypass course filtering rules set of lms in admin dashboard --- .../admin_dashboard/course_reports.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py index 8467b5665f5e..dffd0aa12fac 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py +++ b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py @@ -11,7 +11,9 @@ from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.util.cache import cache_if_anonymous from lms.djangoapps.courseware.access import has_access -from lms.djangoapps.courseware.courses import get_course_by_id, get_courses +from lms.djangoapps.courseware.courses import get_course_by_id +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.lib.api.view_utils import LazySequence def require_user_permission(): @@ -42,6 +44,27 @@ def course_reports(request): courses_list = [] sections = {"key": {}} + def get_courses(user=None): + """ + Retrieve a list of courses that a user has access to based on their permissions. + + This function filters the list of all available courses to include only those + that the specified user has access to in the 'staff', 'instructor' or Global user role. + + Args: + user (optional): The user for whom the course access is being checked. + + Returns: + LazySequence: A lazily evaluated sequence of courses that the user has access to. + The sequence's length is estimated based on the total count of courses. + """ + permissions = ['staff', 'instructor'] + courses = CourseOverview.objects.all() + return LazySequence( + (c for c in courses if any(has_access(user, p, c) for p in permissions)), + est_len=courses.count() + ) + courses_list = get_courses(request.user) course = get_course_by_id(courses_list[0].id, depth=0) From 4310868b623188709e2065fe65b00db20eaa9a7f Mon Sep 17 00:00:00 2001 From: Ahmed Khalid <106074266+ahmed-arb@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:01:36 +0500 Subject: [PATCH 2/9] feat: add new column to all courses enrollment report --- .../admin_dashboard/admin_task/api.py | 1 + .../course_versions/task_helper.py | 2 +- .../admin_dashboard/course_versions/utils.py | 9 +++--- .../wikimedia_general/utils.py | 31 +++++++++++++------ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py b/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py index a30bc0d0142d..841a331e7cc7 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py +++ b/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py @@ -174,6 +174,7 @@ def all_courses_enrollment_report(request): "total_learners_enrolled", "total_learners_completed", "completed_percentage", + "total_cert_generated", ] submit_courses_enrollment_report( request, query_features, report_type, task_all_courses_enrollment_report diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py b/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py index 45b65a0cc95d..efbb21c27bd7 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py +++ b/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py @@ -133,7 +133,7 @@ def upload_all_courses_enrollment_csv( csv_type = task_input.get("csv_type", "all_enrollments_stats") query_features_names = [ - 'Course URL', 'Course title', 'Course available since', 'Parent course URL', 'Parent course title', 'Total learners enrolled', 'Total learners completed', 'Percentage of learners who completed the course', + 'Course URL', 'Course title', 'Course available since', 'Parent course URL', 'Parent course title', 'Total learners enrolled', 'Total learners completed', 'Percentage of learners who completed the course', 'Certificates Generated', ] data = list_all_courses_enrollment_data() diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py b/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py index d03e8dda9196..ca19a060997f 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py +++ b/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py @@ -257,7 +257,7 @@ def list_all_courses_enrollment_data(): continue try: - total_learners_completed, total_learners_enrolled, completed_percentage = \ + course_completion_stats = \ get_course_enrollment_and_completion_stats(course.id) except Exception as e: log.error(f"An error occurred while fetching enrollment data for course ID {course.id}: {e}") @@ -271,9 +271,10 @@ def list_all_courses_enrollment_data(): 'available_since': course.enrollment_start.strftime("%Y-%m-%d") if course.enrollment_start else '', "parent_course_url": parent_course_url, "parent_course_title": parent_course_title, - "total_learners_enrolled": total_learners_enrolled, - "total_learners_completed": total_learners_completed, - "completed_percentage": completed_percentage, + "total_learners_enrolled": course_completion_stats["total_learners_enrolled"], + "total_learners_completed": course_completion_stats["total_learners_completed"], + "completed_percentage": course_completion_stats["completed_percentage"], + "total_cert_generated": course_completion_stats["total_cert_generated"], }) return courses_data diff --git a/openedx/features/wikimedia_features/wikimedia_general/utils.py b/openedx/features/wikimedia_features/wikimedia_general/utils.py index f6cb830b9a52..4d7f2d89f280 100644 --- a/openedx/features/wikimedia_features/wikimedia_general/utils.py +++ b/openedx/features/wikimedia_features/wikimedia_general/utils.py @@ -13,6 +13,7 @@ from django.contrib.auth import get_user_model from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole +from lms.djangoapps.certificates.models import GeneratedCertificate from opaque_keys.edx.keys import CourseKey from openedx.features.course_experience.utils import get_course_outline_block_tree @@ -162,6 +163,11 @@ def is_course_completed(user, course_key): course_key = CourseKey.from_string(course_key) return CourseGradeFactory().read(user, course_key=course_key).summary['grade'] == 'Pass' +def is_certificate_generated(user, course_key): + if isinstance(course_key, str): + course_key = CourseKey.from_string(course_key) + return GeneratedCertificate.objects.filter(user=user, course_id=course_key).exists() + def get_user_completed_course_keys(user): """ @@ -230,23 +236,30 @@ def get_users_course_completion_stats(users, users_enrollments, course_keys): def get_course_enrollment_and_completion_stats(course_id) -> dict: - """Returns the count of student completed the provided course - """ + """Returns the count of student completed the provided course""" enrollments = CourseEnrollment.objects.filter( - course_id=course_id, - is_active=True, - ) + course_id=course_id, + is_active=True, + ) - total_students_completed = 0 + total_learners_completed = 0 + total_cert_generated = 0 for enrollment in enrollments: user = User.objects.get(id=enrollment.user_id) if is_course_completed(user, course_id): - total_students_completed += 1 + total_learners_completed += 1 + if is_certificate_generated(user, course_id): + total_cert_generated += 1 enrollment_count = enrollments.count() - percentage_completed = (total_students_completed/enrollment_count) * 100 if enrollment_count else 0 + completed_percentage = (total_learners_completed / enrollment_count) * 100 if enrollment_count else 0 - return (total_students_completed, enrollment_count, percentage_completed) + return { + "total_learners_completed": total_learners_completed, + "total_cert_generated": total_cert_generated, + "total_learners_enrolled": enrollment_count, + "completed_percentage": completed_percentage, + } def get_paced_type(self_paced): From 5669f87cb8024a1129942aa07f215e6866509345 Mon Sep 17 00:00:00 2001 From: Ahmed Khalid <106074266+ahmed-arb@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:54:05 +0500 Subject: [PATCH 3/9] feat: add users enrollment report in admin dashboard --- .../admin_dashboard/admin_task/api.py | 22 ++++++++++ .../admin_dashboard/course_reports.py | 1 + .../course_versions/task_helper.py | 41 +++++++++++++++++++ .../admin_dashboard/course_versions/utils.py | 30 ++++++++++++-- .../admin_dashboard/js/course_reports.js | 4 ++ .../admin_dashboard/tasks.py | 21 +++++++++- .../course_report/course-reports.html | 15 +++++++ .../admin_dashboard/urls.py | 7 +++- .../wikimedia_general/utils.py | 14 +++++-- 9 files changed, 147 insertions(+), 8 deletions(-) diff --git a/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py b/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py index 841a331e7cc7..78c5536f1601 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py +++ b/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py @@ -25,6 +25,7 @@ task_courses_enrollment_report, task_all_courses_enrollment_report, task_user_pref_lang_report, + task_users_enrollment_info_report, ) from openedx.features.wikimedia_features.admin_dashboard.course_versions import task_helper @@ -236,6 +237,27 @@ def user_pref_lang_report(request): return JsonResponse({"status": success_status}) +@transaction.non_atomic_requests +@require_POST +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +def users_enrollment_report(request): + """ + Handles request to generate CSV of all users enrollments info. + """ + report_type = _("all_users_enrollment") + success_status = SUCCESS_MESSAGE_TEMPLATE.format( + report_type="Users Enrollments Report" + ) + task_input = { + 'features': ["username", "enrollments_count", "completions_count"], + 'csv_type': report_type, + } + + submit_task(request, report_type, task_users_enrollment_info_report, 'all_courses', task_input, "") + return JsonResponse({"status": success_status}) + + def submit_average_calculate_grades_csv(request, course_key): """ AlreadyRunningError is raised if the course's grades are already being updated. diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py index dffd0aa12fac..cfd2506609b7 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py +++ b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py @@ -106,6 +106,7 @@ def section_data_download(course, access): 'courses_enrollments_csv_url': reverse('admin_dashboard:courses_enrollment_report'), 'all_courses_enrollments_csv_url': reverse('admin_dashboard:all_courses_enrollment_report'), 'user_pref_lang_csv_url': reverse('admin_dashboard:user_pref_lang_report'), + 'users_enrollment_url': reverse('admin_dashboard:users_enrollment_report'), } if not access.get('data_researcher'): section_data['is_hidden'] = True diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py b/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py index efbb21c27bd7..4ccff4df716a 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py +++ b/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py @@ -15,6 +15,7 @@ get_quarter_dates, list_all_courses_enrollment_data, list_user_pref_lang, + list_users_enrollments, list_version_report_info_per_course, list_version_report_info_total, list_quarterly_courses_enrollment_data, @@ -252,3 +253,43 @@ def upload_user_pref_lang_csv( report_store.store_rows(course_id_str, report_name, rows) return task_progress.update_task_state(extra_meta=current_step) + + +def upload_users_enrollment_info_csv( + _xmodule_instance_args, _entry_id, course_id_str, task_input, action_name, user_ids +): + """ + Generate a CSV file containing information of users enrollments and course completions. + """ + start_time = time() + start_date = datetime.now(UTC) + num_reports = 1 + task_progress = TaskProgress(action_name, num_reports, start_time) + current_step = {"step": "Getting Users profile info"} + task_progress.update_task_state(extra_meta=current_step) + + # Compute result table and format it + query_features = task_input.get("features") + + query_features_names = ["Username", "Enrollments", "Courses Completed"] + + data = list_users_enrollments() + __, rows = format_dictlist(data, query_features) + + task_progress.attempted = task_progress.succeeded = len(rows) + task_progress.skipped = task_progress.total - task_progress.attempted + + rows.insert(0, query_features_names) + + current_step = {"step": "Uploading CSV"} + task_progress.update_task_state(extra_meta=current_step) + + # Perform the upload + report_store = ReportStore.from_config("GRADES_DOWNLOAD") + csv_name = task_input.get("csv_type") + report_name = "{csv_name}_{timestamp_str}.csv".format( + csv_name=csv_name, timestamp_str=start_date.strftime("%Y-%m-%d-%H%M") + ) + report_store.store_rows(course_id_str, report_name, rows) + + return task_progress.update_task_state(extra_meta=current_step) diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py b/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py index ca19a060997f..533bfe2e4f61 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py +++ b/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py @@ -6,6 +6,7 @@ from django.conf import settings from django.db.models import OuterRef, Subquery, Value, TextField from django.db.models.functions import Coalesce +from django.db.models import Prefetch from common.djangoapps.student.models import CourseEnrollment @@ -18,7 +19,7 @@ from openedx.core.djangoapps.user_api.models import UserPreference from openedx.features.wikimedia_features.meta_translations.models import CourseTranslation from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.features.wikimedia_features.wikimedia_general.utils import get_course_enrollment_and_completion_stats +from openedx.features.wikimedia_features.wikimedia_general.utils import get_course_enrollment_and_completion_stats, get_user_course_completions from edx_proctoring.api import get_last_exam_completion_date @@ -237,7 +238,7 @@ def list_all_courses_enrollment_data(): Get all courses enrollment report """ - courses = get_visible_courses() + courses = CourseOverview.objects.all() courses_data = [] for course in courses: @@ -280,7 +281,6 @@ def list_all_courses_enrollment_data(): return courses_data - def list_quarterly_courses_enrollment_data(quarter): """ Get course reports @@ -365,3 +365,27 @@ def list_user_pref_lang(): pref_lang_data.append(pref_lang) return pref_lang_data + + +def list_users_enrollments(): + + users_with_course_enrollments = User.objects.prefetch_related("courseenrollment_set__course").filter( + courseenrollment__is_active=1 + ).distinct() + + users_enrollments_data = [] + + for user in users_with_course_enrollments: + user_enrollments = user.courseenrollment_set.all() + user_completions = get_user_course_completions(user, user_enrollments) + user_enrollments_count = user_enrollments.count() + + users_enrollments_data.append( + { + "username": user.username, + "enrollments_count": user_enrollments_count, + "completions_count": user_completions, + } + ) + + return users_enrollments_data diff --git a/openedx/features/wikimedia_features/admin_dashboard/static/admin_dashboard/js/course_reports.js b/openedx/features/wikimedia_features/admin_dashboard/static/admin_dashboard/js/course_reports.js index 8fb277a5a4f1..5a1735029899 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/static/admin_dashboard/js/course_reports.js +++ b/openedx/features/wikimedia_features/admin_dashboard/static/admin_dashboard/js/course_reports.js @@ -328,6 +328,10 @@ let data_url = $(this).attr('data-endpoint'); AjaxCall(data_url); }) + $("[name='users-enrollment-csv']").click(function() { + let data_url = $(this).attr('data-endpoint'); + AjaxCall(data_url); + }) $(document).ready(function() { endpoint = '/wikimedia/list_all_courses_report_downloads'; ReportDownloads(); diff --git a/openedx/features/wikimedia_features/admin_dashboard/tasks.py b/openedx/features/wikimedia_features/admin_dashboard/tasks.py index 6fc8176ab18b..365964cbc687 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/tasks.py +++ b/openedx/features/wikimedia_features/admin_dashboard/tasks.py @@ -37,6 +37,7 @@ upload_quarterly_courses_enrollment_csv, upload_all_courses_enrollment_csv, upload_user_pref_lang_csv, + upload_users_enrollment_info_csv, ) from openedx.features.wikimedia_features.email.utils import send_notification @@ -130,7 +131,7 @@ def task_courses_enrollment_report(entry_id, xmodule_instance_args, user_id): @set_code_owner_attribute def task_user_pref_lang_report(entry_id, xmodule_instance_args, user_id): """ - Generate a course enrollment report for users preferred languages and push the results to an S3 bucket for download. + Generate a report for users preferred languages and push the results to an S3 bucket for download. """ # Translators: This is a past-tense verb that is inserted into task progress messages as {action}. action_name = ugettext_noop('generated') @@ -142,6 +143,24 @@ def task_user_pref_lang_report(entry_id, xmodule_instance_args, user_id): return run_main_task(entry_id, task_fn, action_name, user_id) +@shared_task(base=BaseAdminReportTask) +@set_code_owner_attribute +def task_users_enrollment_info_report(entry_id, xmodule_instance_args, user_id): + """ + Generate a report for users enrollments and course completions. + """ + # Translators: This is a past-tense verb that is inserted into task progress messages as {action}. + action_name = ugettext_noop("generated") + TASK_LOG.info( + "Task: %s, AdminReportTask ID: %s, Task type: %s, Preparing for task execution", + xmodule_instance_args.get("task_id"), + entry_id, + action_name, + ) + task_fn = partial(upload_users_enrollment_info_csv, xmodule_instance_args) + return run_main_task(entry_id, task_fn, action_name, user_id) + + @task(base=LoggedTask) def send_report_ready_email_task(msg_class_key, context, subject, email): TASK_LOG.info("Initiated task to send admin report ready notifications.") diff --git a/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html b/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html index 1115f6e4233a..1568324ac21a 100644 --- a/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html +++ b/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html @@ -223,6 +223,21 @@ >${_("Generate")} + + + ${_("All Users enrollment report")} + ${_("Generates a report on users enrollments and their completed courses")} + + + + + + - - - ${_("Quarterly courses enrollment report")} - ${_("Generate a report for last quarter enrollments of all courses.")} - - - - - - - - - - - - - ${_("All courses enrollment report")} - ${_("Generate an all time report of enrollment/completion for all courses.")} - - - - - - - - - ${_("Users preferred language report")} - ${_("Generate a report on users preferred lanugage. Displays N/A for users who have not selected a lanugage")} - - - - - - - - - ${_("All Users enrollment report")} - ${_("Generates a report on users enrollments and their completed courses")} - - - - - -