', {
class: 'slickgrid'
});
+ $tablePlaceholder.css("height", "300px");
this.$report_downloads_table.append($tablePlaceholder);
grid = new Slick.Grid($tablePlaceholder, reportDownloadsData, columns, options);
grid.onClick.subscribe(function(event) {
diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss
index 3bd8e8a0c251..dcc04c72ba4b 100644
--- a/lms/static/sass/course/instructor/_instructor_2.scss
+++ b/lms/static/sass/course/instructor/_instructor_2.scss
@@ -1504,6 +1504,16 @@
font-style: italic;
font-size: 0.9em;
}
+ .report-downloads-table {
+ .slickgrid {
+ height: 300px;
+ padding: ($baseline/4);
+ }
+ // Disable horizontal scroll bar when grid only has 1 column. Remove this CSS class when more columns added.
+ .slick-viewport {
+ overflow-x: hidden !important;
+ }
+ }
}
// view - data download
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html
index 20950eca11f1..c9c15269f0da 100644
--- a/lms/templates/instructor/instructor_dashboard_2/data_download.html
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html
@@ -29,6 +29,31 @@
${_("Reports")}
${_("Please be patient and do not click these buttons multiple times. Clicking these buttons multiple times will significantly slow the generation process.")}
+ %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
+
${_("Click to generate a CSV grade report for all currently enrolled students.")}
+
+
+
+
+
+
+
+
${_("Click to generate a ZIP file that contains all submission texts and attachments.")}
+
+
+
+ %endif
+
+
${_("Click to list certificates that are issued for this course:")}
+
+
+
+
+
+
+
+
+
${_("Click to generate a CSV file of all students enrolled in this course, along with profile information such as email address and username:")}
@@ -76,37 +101,12 @@
${_("Reports")}
)}
-
-
${_("Click to list certificates that are issued for this course:")}
-
-
-
-
-
-
-
-
% if not disable_buttons:
${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}
%endif
- %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
-
${_("Click to generate a CSV grade report for all currently enrolled students.")}
-
-
-
-
-
-
-
-
${_("Click to generate a ZIP file that contains all submission texts and attachments.")}
-
-
-
- %endif
-
diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html
index ec21e3a99316..b7465269d297 100644
--- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html
+++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html
@@ -1,5 +1,8 @@
<%page args="section_data" expression_filter="h"/>
-<%! from django.utils.translation import ugettext as _ %>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.markup import HTML, Text
+%>
%if section_data['access']['staff'] or section_data['access']['instructor']:
%if section_data.get('writable_gradebook_url') or section_data.get('is_small_course'):
@@ -13,9 +16,12 @@
+ ${_("The reports listed below are available for download, identified by UTC date and time of generation.")}
+
+
+ ## Translators: a table of URL links to report files appears after this sentence.
+
+ ${Text(_("{strong_start}Note{strong_end}: {ul_start}{li_start}To keep student data secure, you cannot save or email these links for direct access. Copies of links expire within 5 minutes.{li_end}{li_start}Report files are deleted 90 days after generation. If you will need access to old reports, download and store the files, in accordance with your institution's data security policies.{li_end}{ul_end}")).format(
+ strong_start=HTML(""),
+ strong_end=HTML(""),
+ ul_start=HTML("
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..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
@@ -174,6 +175,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
@@ -235,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 8467b5665f5e..0e17d5b753ed 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)
@@ -83,7 +106,8 @@ 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'):
+ if not (access.get('data_researcher') or access.get('staff') or access.get('instructor')):
section_data['is_hidden'] = True
return section_data
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..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,
@@ -133,7 +134,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()
@@ -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 d03e8dda9196..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:
@@ -257,7 +258,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,15 +272,15 @@ 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
-
def list_quarterly_courses_enrollment_data(quarter):
"""
Get course reports
@@ -364,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..df26fe2741a9 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
@@ -34,6 +34,78 @@
+
+
+
+ ${_("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")}
+
+
+
+
+
+
${_("List profile information for enrolled students")}
@@ -167,62 +239,6 @@
-
-
- ${_("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")}
-