Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1562,6 +1562,44 @@ def post(self, request, course_id, csv=False): # pylint: disable=redefined-oute
return JsonResponse({"status": success_status})


@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
@method_decorator(transaction.non_atomic_requests, name='dispatch')
class GetXblocksList(DeveloperErrorViewMixin, APIView):
"""
Respond with a csv which contains a summary of all xblocks in all courses.
"""
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.CAN_RESEARCH

@method_decorator(ensure_csrf_cookie)
@method_decorator(transaction.non_atomic_requests)
def post(self, request, course_id): # pylint: disable=redefined-outer-name
"""
Handle POST requests

Args:
request: The HTTP request object.
course_id: The ID of the course for which to retrieve student information.

Returns:
Response: A CSV response containing xblocks information.
"""
course_key = CourseKey.from_string(course_id)
course = get_course_by_id(course_key)
report_type = _('xblocks list')

try:
task_api.submit_xblocks_list_csv(
request,
course_key,
)
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')
@method_decorator(transaction.non_atomic_requests, name='dispatch')
class GetStudentsWhoMayEnroll(DeveloperErrorViewMixin, APIView):
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/instructor/views/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
path('get_problem_responses', api.GetProblemResponses.as_view(), name='get_problem_responses'),
path('get_issued_certificates/', api.GetIssuedCertificates.as_view(), name='get_issued_certificates'),
re_path(r'^get_students_features(?P<csv>/csv)?$', api.GetStudentsFeatures.as_view(), name='get_students_features'),
re_path(r'^get_xblocks_list$', api.GetXblocksList.as_view(), name='get_xblocks_list'),
path('get_grading_config', api.GetGradingConfig.as_view(), name='get_grading_config'),
path('get_students_who_may_enroll', api.GetStudentsWhoMayEnroll.as_view(), name='get_students_who_may_enroll'),
path('get_enrolled_students_with_inactive_account', api.GetInactiveEnrolledStudents.as_view(),
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/instructor/views/instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ def _section_data_download(course, access):
'export_ora2_submission_files', kwargs={'course_id': str(course_key)}
),
'export_ora2_summary_url': reverse('export_ora2_summary', kwargs={'course_id': str(course_key)}),
'export_xblock_list_url': reverse('get_xblocks_list', kwargs={'course_id': str(course_key)}),
}
if not access.get('data_researcher'):
section_data['is_hidden'] = True
Expand Down
15 changes: 15 additions & 0 deletions lms/djangoapps/instructor_task/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
calculate_problem_grade_report,
calculate_problem_responses_csv,
calculate_students_features_csv,
calculate_xblock_list_csv,
cohort_students,
course_survey_report_csv,
delete_problem_state,
Expand Down Expand Up @@ -395,6 +396,20 @@ def submit_calculate_students_features_csv(request, course_key, features, **task
return submit_task(request, task_type, task_class, course_key, task_input, task_key)


def submit_xblocks_list_csv(request, course_key):
"""
Submits a task to generate a CSV containing info about xblocks in courses.

Raises AlreadyRunningError if said CSV is already being updated.
"""
task_type = InstructorTaskTypes.XBLOCK_LIST_CSV
task_class = calculate_xblock_list_csv
task_input = {}
task_key = ""

return submit_task(request, task_type, task_class, course_key, task_input, task_key)


def submit_calculate_may_enroll_csv(request, course_key, features):
"""
Submits a task to generate a CSV file containing information about
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/instructor_task/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class InstructorTaskTypes(str, Enum):
PROBLEM_RESPONSES_CSV = "problem_responses_csv"
PROCTORED_EXAM_RESULTS_REPORT = "proctored_exam_results_report"
PROFILE_INFO_CSV = "profile_info_csv"
XBLOCK_LIST_CSV = "xblock_list_csv"
REGENERATE_CERTIFICATES_ALL_STUDENT = "regenerate_certificates_all_student"
RESCORE_PROBLEM = "rescore_problem"
RESCORE_PROBLEM_IF_HIGHER = "rescore_problem_if_higher"
Expand Down
14 changes: 14 additions & 0 deletions lms/djangoapps/instructor_task/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from lms.djangoapps.bulk_email.tasks import perform_delegate_email_batches
from lms.djangoapps.instructor_task.tasks_base import BaseInstructorTask
from lms.djangoapps.instructor_task.tasks_helper.certs import generate_students_certificates
from lms.djangoapps.instructor_task.tasks_helper.components import upload_xblock_list_csv
from lms.djangoapps.instructor_task.tasks_helper.enrollments import (
upload_inactive_enrolled_students_info_csv,
upload_may_enroll_csv,
Expand Down Expand Up @@ -232,6 +233,19 @@ def calculate_students_features_csv(entry_id, xblock_instance_args):
return run_main_task(entry_id, task_fn, action_name)


@shared_task(base=BaseInstructorTask)
@set_code_owner_attribute
def calculate_xblock_list_csv(entry_id, xblock_instance_args):
"""
Compute list of components/lxblocks and upload the
CSV to an S3 bucket for download.
"""
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
action_name = gettext_noop('generated')
task_fn = partial(upload_xblock_list_csv, xblock_instance_args)
return run_main_task(entry_id, task_fn, action_name)


@shared_task(base=BaseInstructorTask)
@set_code_owner_attribute
def course_survey_report_csv(entry_id, xblock_instance_args):
Expand Down
88 changes: 88 additions & 0 deletions lms/djangoapps/instructor_task/tasks_helper/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""
Instructor tasks related to components and xblocks in the course.
"""

from datetime import datetime
from time import time
from pytz import UTC
from lms.djangoapps.instructor_analytics.csvs import format_dictlist
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from xmodule.modulestore.django import modulestore

from .runner import TaskProgress
from .utils import upload_csv_to_report_store


def upload_xblock_list_csv(
_xblock_instance_args, _entry_id, course_id, task_input, action_name
):
"""
Generate a csv containing all components.
"""
start_time = time()
start_date = datetime.now(UTC)

overviews = CourseOverview.objects.filter(id=course_id).order_by("id")
# NOTE: if we want to report on all courses at once, use this line below instead of the one above
# overviews = CourseOverview.objects.all().order_by('id')

total_courses = overviews.count()
task_progress = TaskProgress(action_name, total_courses, start_time)

current_step = {"step": "Calculating XBlock Info"}
task_progress.update_task_state(extra_meta=current_step)

data = []
succeeded_count = 0
for overview in overviews:
try:
course = modulestore().get_course(overview.id)
data.extend(
{
"Course ID": course.id,
"Course Name": course.display_name,
"Section Name": section.display_name,
"Subsection Name": subsection.display_name,
"Unit Name": unit.display_name,
"Component Name": component.display_name,
"Xblock Type": component.location.block_type,
}
for section in course.get_children()
for subsection in section.get_children()
for unit in subsection.get_children()
for component in unit.get_children()
)
succeeded_count += 1
except: # pylint: disable=bare-except
print(f"FAILED GETTING COURSE {overview.id} FROM MODULESTORE")

header, rows = format_dictlist(
data,
[
"Course ID",
"Course Name",
"Section Name",
"Subsection Name",
"Unit Name",
"Component Name",
"Xblock Type",
],
)

task_progress.attempted = total_courses
task_progress.succeeded = succeeded_count
task_progress.skipped = task_progress.failed = total_courses - succeeded_count

rows.insert(0, header)

current_step = {"step": "Uploading CSV"}
task_progress.update_task_state(extra_meta=current_step)

# Perform the upload
upload_parent_dir = task_input.get("upload_parent_dir", "")
upload_filename = task_input.get("filename", "xblocks_list")
upload_csv_to_report_store(
rows, upload_filename, course_id, start_date, parent_dir=upload_parent_dir
)

return task_progress.update_task_state(extra_meta=current_step)
26 changes: 26 additions & 0 deletions lms/static/js/instructor_dashboard/data_download.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
this.$calculate_grades_csv_btn = this.$section.find("input[name='calculate-grades-csv']");
this.$problem_grade_report_csv_btn = this.$section.find("input[name='problem-grade-report']");
this.$async_report_btn = this.$section.find("input[class='async-report-btn']");
this.$export_xblocks_csv_btn = this.$section.find("input[name='export-xblocks-csv']");
this.$download = this.$section.find('.data-download-container');
this.$download_display_text = this.$download.find('.data-display-text');
this.$download_request_response_error = this.$download.find('.request-response-error');
Expand Down Expand Up @@ -400,6 +401,31 @@
}
});
});
this.$export_xblocks_csv_btn.click(function() {
var url = dataDownloadObj.$export_xblocks_csv_btn.data('endpoint');
var errorMessage = gettext('Error generating xblocks information. Please try again.');
dataDownloadObj.clear_display();
return $.ajax({
type: 'POST',
dataType: 'json',
url: url,
error: function(error) {
if (error.responseText) {
errorMessage = JSON.parse(error.responseText);
}
dataDownloadObj.$reports_request_response_error.text(errorMessage);
return dataDownloadObj.$reports_request_response_error.css({
display: 'block'
});
},
success: function(data) {
dataDownloadObj.$reports_request_response.text(data.status);
return $('.msg-confirm').css({
display: 'block'
});
}
});
});
}

InstructorDashboardDataDownload.prototype.onClickTitle = function() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ <h3 class="hd hd-3">${_("Reports")}</h3>

<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>

<p>${_("Click to generate a CSV file of all components with their XBlock types for the course.")}</p>

<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>

%endif

<div class="request-response msg msg-confirm copy" id="report-request-response"></div>
Expand Down
Loading