Skip to content

Commit 0732ee0

Browse files
committed
feat: Add a csv report of xblocks used in courses
Private-ref: https://tasks.opencraft.com/browse/BB-10225
1 parent f4f14a6 commit 0732ee0

File tree

9 files changed

+188
-0
lines changed

9 files changed

+188
-0
lines changed

lms/djangoapps/instructor/views/api.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,44 @@ def post(self, request, course_id, csv=False): # pylint: disable=redefined-oute
15621562
return JsonResponse({"status": success_status})
15631563

15641564

1565+
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
1566+
@method_decorator(transaction.non_atomic_requests, name='dispatch')
1567+
class GetXblocksList(DeveloperErrorViewMixin, APIView):
1568+
"""
1569+
Respond with a csv which contains a summary of all xblocks in all courses.
1570+
"""
1571+
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
1572+
permission_name = permissions.CAN_RESEARCH
1573+
1574+
@method_decorator(ensure_csrf_cookie)
1575+
@method_decorator(transaction.non_atomic_requests)
1576+
def post(self, request, course_id): # pylint: disable=redefined-outer-name
1577+
"""
1578+
Handle POST requests
1579+
1580+
Args:
1581+
request: The HTTP request object.
1582+
course_id: The ID of the course for which to retrieve student information.
1583+
1584+
Returns:
1585+
Response: A CSV response containing xblocks information.
1586+
"""
1587+
course_key = CourseKey.from_string(course_id)
1588+
course = get_course_by_id(course_key)
1589+
report_type = _('xblocks list')
1590+
1591+
try:
1592+
task_api.submit_xblocks_list_csv(
1593+
request,
1594+
course_key,
1595+
)
1596+
success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
1597+
except Exception as e:
1598+
raise self.api_error(status.HTTP_400_BAD_REQUEST, str(e), 'Requested task is already running')
1599+
1600+
return JsonResponse({"status": success_status})
1601+
1602+
15651603
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
15661604
@method_decorator(transaction.non_atomic_requests, name='dispatch')
15671605
class GetStudentsWhoMayEnroll(DeveloperErrorViewMixin, APIView):

lms/djangoapps/instructor/views/api_urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
path('get_problem_responses', api.GetProblemResponses.as_view(), name='get_problem_responses'),
3030
path('get_issued_certificates/', api.GetIssuedCertificates.as_view(), name='get_issued_certificates'),
3131
re_path(r'^get_students_features(?P<csv>/csv)?$', api.GetStudentsFeatures.as_view(), name='get_students_features'),
32+
re_path(r'^get_xblocks_list$', api.GetXblocksList.as_view(), name='get_xblocks_list'),
3233
path('get_grading_config', api.GetGradingConfig.as_view(), name='get_grading_config'),
3334
path('get_students_who_may_enroll', api.GetStudentsWhoMayEnroll.as_view(), name='get_students_who_may_enroll'),
3435
path('get_enrolled_students_with_inactive_account', api.GetInactiveEnrolledStudents.as_view(),

lms/djangoapps/instructor/views/instructor_dashboard.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@ def _section_data_download(course, access):
670670
'export_ora2_submission_files', kwargs={'course_id': str(course_key)}
671671
),
672672
'export_ora2_summary_url': reverse('export_ora2_summary', kwargs={'course_id': str(course_key)}),
673+
'export_xblock_list_url': reverse('get_xblocks_list', kwargs={'course_id': str(course_key)}),
673674
}
674675
if not access.get('data_researcher'):
675676
section_data['is_hidden'] = True

lms/djangoapps/instructor_task/api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
calculate_problem_grade_report,
3838
calculate_problem_responses_csv,
3939
calculate_students_features_csv,
40+
calculate_xblock_list_csv,
4041
cohort_students,
4142
course_survey_report_csv,
4243
delete_problem_state,
@@ -395,6 +396,20 @@ def submit_calculate_students_features_csv(request, course_key, features, **task
395396
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
396397

397398

399+
def submit_xblocks_list_csv(request, course_key):
400+
"""
401+
Submits a task to generate a CSV containing info about xblocks in courses.
402+
403+
Raises AlreadyRunningError if said CSV is already being updated.
404+
"""
405+
task_type = InstructorTaskTypes.XBLOCK_LIST_CSV
406+
task_class = calculate_xblock_list_csv
407+
task_input = {}
408+
task_key = ""
409+
410+
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
411+
412+
398413
def submit_calculate_may_enroll_csv(request, course_key, features):
399414
"""
400415
Submits a task to generate a CSV file containing information about

lms/djangoapps/instructor_task/data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class InstructorTaskTypes(str, Enum):
2929
PROBLEM_RESPONSES_CSV = "problem_responses_csv"
3030
PROCTORED_EXAM_RESULTS_REPORT = "proctored_exam_results_report"
3131
PROFILE_INFO_CSV = "profile_info_csv"
32+
XBLOCK_LIST_CSV = "xblock_list_csv"
3233
REGENERATE_CERTIFICATES_ALL_STUDENT = "regenerate_certificates_all_student"
3334
RESCORE_PROBLEM = "rescore_problem"
3435
RESCORE_PROBLEM_IF_HIGHER = "rescore_problem_if_higher"

lms/djangoapps/instructor_task/tasks.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from lms.djangoapps.bulk_email.tasks import perform_delegate_email_batches
3131
from lms.djangoapps.instructor_task.tasks_base import BaseInstructorTask
3232
from lms.djangoapps.instructor_task.tasks_helper.certs import generate_students_certificates
33+
from lms.djangoapps.instructor_task.tasks_helper.components import upload_xblock_list_csv
3334
from lms.djangoapps.instructor_task.tasks_helper.enrollments import (
3435
upload_inactive_enrolled_students_info_csv,
3536
upload_may_enroll_csv,
@@ -232,6 +233,19 @@ def calculate_students_features_csv(entry_id, xblock_instance_args):
232233
return run_main_task(entry_id, task_fn, action_name)
233234

234235

236+
@shared_task(base=BaseInstructorTask)
237+
@set_code_owner_attribute
238+
def calculate_xblock_list_csv(entry_id, xblock_instance_args):
239+
"""
240+
Compute list of xblocks for all courses and upload the
241+
CSV to an S3 bucket for download.
242+
"""
243+
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
244+
action_name = gettext_noop('generated')
245+
task_fn = partial(upload_xblock_list_csv, xblock_instance_args)
246+
return run_main_task(entry_id, task_fn, action_name)
247+
248+
235249
@shared_task(base=BaseInstructorTask)
236250
@set_code_owner_attribute
237251
def course_survey_report_csv(entry_id, xblock_instance_args):
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""
2+
Instructor tasks related to components and xblocks in the course.
3+
"""
4+
5+
from datetime import datetime
6+
from time import time
7+
from pytz import UTC
8+
from lms.djangoapps.instructor_analytics.csvs import format_dictlist
9+
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
10+
from xmodule.modulestore.django import modulestore
11+
12+
from .runner import TaskProgress
13+
from .utils import upload_csv_to_report_store
14+
15+
16+
def upload_xblock_list_csv(
17+
_xblock_instance_args, _entry_id, course_id, task_input, action_name
18+
):
19+
"""
20+
generate a csv containing all xblocks in all courses
21+
"""
22+
start_time = time()
23+
start_date = datetime.now(UTC)
24+
25+
overviews = CourseOverview.objects.filter(id=course_id).order_by("id")
26+
# NOTE: if we want to report on all courses at once, use this line below instead of the one above
27+
# overviews = CourseOverview.objects.all().order_by('id')
28+
29+
total_courses = overviews.count()
30+
task_progress = TaskProgress(action_name, total_courses, start_time)
31+
32+
current_step = {"step": "Calculating XBlock Info"}
33+
task_progress.update_task_state(extra_meta=current_step)
34+
35+
data = []
36+
succeeded_count = 0
37+
for overview in overviews:
38+
try:
39+
course = modulestore().get_course(overview.id)
40+
data.extend(
41+
{
42+
"Course ID": course.id,
43+
"Course Name": course.display_name,
44+
"Section Name": section.display_name,
45+
"Subsection Name": subsection.display_name,
46+
"Unit Name": unit.display_name,
47+
"Component Name": component.display_name,
48+
"Xblock Type": component.location.block_type,
49+
}
50+
for section in course.get_children()
51+
for subsection in section.get_children()
52+
for unit in subsection.get_children()
53+
for component in unit.get_children()
54+
)
55+
succeeded_count += 1
56+
except: # pylint: disable=bare-except
57+
print(f"FAILED GETTING COURSE {overview.id} FROM MODULESTORE")
58+
59+
header, rows = format_dictlist(
60+
data,
61+
[
62+
"Course ID",
63+
"Course Name",
64+
"Section Name",
65+
"Subsection Name",
66+
"Unit Name",
67+
"Component Name",
68+
"Xblock Type",
69+
],
70+
)
71+
72+
task_progress.attempted = total_courses
73+
task_progress.succeeded = succeeded_count
74+
task_progress.skipped = task_progress.failed = total_courses - succeeded_count
75+
76+
rows.insert(0, header)
77+
78+
current_step = {"step": "Uploading CSV"}
79+
task_progress.update_task_state(extra_meta=current_step)
80+
81+
# Perform the upload
82+
upload_parent_dir = task_input.get("upload_parent_dir", "")
83+
upload_filename = task_input.get("filename", "xblocks_list")
84+
upload_csv_to_report_store(
85+
rows, upload_filename, course_id, start_date, parent_dir=upload_parent_dir
86+
)
87+
88+
return task_progress.update_task_state(extra_meta=current_step)

lms/static/js/instructor_dashboard/data_download.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
this.$calculate_grades_csv_btn = this.$section.find("input[name='calculate-grades-csv']");
110110
this.$problem_grade_report_csv_btn = this.$section.find("input[name='problem-grade-report']");
111111
this.$async_report_btn = this.$section.find("input[class='async-report-btn']");
112+
this.$export_xblocks_csv_btn = this.$section.find("input[name='export-xblocks-csv']");
112113
this.$download = this.$section.find('.data-download-container');
113114
this.$download_display_text = this.$download.find('.data-display-text');
114115
this.$download_request_response_error = this.$download.find('.request-response-error');
@@ -400,6 +401,31 @@
400401
}
401402
});
402403
});
404+
this.$export_xblocks_csv_btn.click(function() {
405+
var url = dataDownloadObj.$export_xblocks_csv_btn.data('endpoint');
406+
var errorMessage = gettext('Error generating xblocks information. Please try again.');
407+
dataDownloadObj.clear_display();
408+
return $.ajax({
409+
type: 'POST',
410+
dataType: 'json',
411+
url: url,
412+
error: function(error) {
413+
if (error.responseText) {
414+
errorMessage = JSON.parse(error.responseText);
415+
}
416+
dataDownloadObj.$reports_request_response_error.text(errorMessage);
417+
return dataDownloadObj.$reports_request_response_error.css({
418+
display: 'block'
419+
});
420+
},
421+
success: function(data) {
422+
dataDownloadObj.$reports_request_response.text(data.status);
423+
return $('.msg-confirm').css({
424+
display: 'block'
425+
});
426+
}
427+
});
428+
});
403429
}
404430

405431
InstructorDashboardDataDownload.prototype.onClickTitle = function() {

lms/templates/instructor/instructor_dashboard_2/data_download.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ <h3 class="hd hd-3">${_("Reports")}</h3>
109109

110110
<p><input type="button" name="export-ora2-data" class="async-report-btn" value="${_("Generate Submission Files Archive")}" data-endpoint="${ section_data['export_ora2_submission_files_url'] }"/></p>
111111

112+
<p>${_("Click to generate a CSV file of all componets with their xblock types for the course.")}</p>
113+
114+
<p><input type="button" name="export-xblocks-csv" value="${_("Download XBlocks list as a CSV")}" data-endpoint="${ section_data['export_xblock_list_url'] }" data-csv="true"></p>
115+
112116
%endif
113117

114118
<div class="request-response msg msg-confirm copy" id="report-request-response"></div>

0 commit comments

Comments
 (0)