feat: add data report for enrolled & inactive user
Add new data report for learners who are enrolled in a course and have not activated their account
This commit is contained in:
committed by
Muhammad Faraz Maqsood
parent
25f9397683
commit
5879a52b72
@@ -1598,6 +1598,46 @@ class GetStudentsWhoMayEnroll(DeveloperErrorViewMixin, APIView):
|
||||
raise MethodNotAllowed('GET')
|
||||
|
||||
|
||||
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
|
||||
@method_decorator(transaction.non_atomic_requests, name='dispatch')
|
||||
class GetInactiveEnrolledStudents(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
Initiate generation of a CSV file containing information about
|
||||
students who are enrolled in a course but have inactive account.
|
||||
"""
|
||||
|
||||
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):
|
||||
"""
|
||||
Initiate generation of a CSV file containing information about
|
||||
students who are enrolled in a course but have inactive account.
|
||||
|
||||
Responds with JSON
|
||||
{"status": "... status message ..."}
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
query_features = ["email"]
|
||||
report_type = _("inactive enrollment")
|
||||
try:
|
||||
task_api.submit_calculate_inactive_enrolled_students_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})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed("GET")
|
||||
|
||||
|
||||
def _cohorts_csv_validator(file_storage, file_to_validate):
|
||||
"""
|
||||
Verifies that the expected columns are present in the CSV used to add users to cohorts.
|
||||
|
||||
@@ -31,6 +31,8 @@ urlpatterns = [
|
||||
re_path(r'^get_students_features(?P<csv>/csv)?$', api.GetStudentsFeatures.as_view(), name='get_students_features'),
|
||||
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(),
|
||||
name='get_enrolled_students_with_inactive_account'),
|
||||
path('get_anon_ids', api.GetAnonIds.as_view(), name='get_anon_ids'),
|
||||
path('get_student_enrollment_status', api.GetStudentEnrollmentStatus.as_view(),
|
||||
name="get_student_enrollment_status"),
|
||||
|
||||
@@ -650,6 +650,9 @@ def _section_data_download(course, access):
|
||||
'get_students_who_may_enroll_url': reverse(
|
||||
'get_students_who_may_enroll', kwargs={'course_id': str(course_key)}
|
||||
),
|
||||
'get_inactive_enrolled_students_url': reverse(
|
||||
'get_enrolled_students_with_inactive_account', kwargs={'course_id': str(course_key)}
|
||||
),
|
||||
'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': str(course_key)}),
|
||||
'list_proctored_results_url': reverse(
|
||||
'get_proctored_exam_results', kwargs={'course_id': str(course_key)}
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Count # lint-amnesty, pylint: disable=unused-import
|
||||
from django.db.models import Count, F
|
||||
from django.urls import reverse
|
||||
from edx_proctoring.api import get_exam_violation_report
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
@@ -219,6 +219,34 @@ def list_may_enroll(course_key, features):
|
||||
return [extract_student(student, features) for student in may_enroll_and_unenrolled]
|
||||
|
||||
|
||||
def list_inactive_enrolled_students(course_key, features):
|
||||
"""
|
||||
Return info about students who are enrolled in a course but have not activated their account.
|
||||
|
||||
list_enrolled_inactive_students(course_key, ['email'])
|
||||
would return [
|
||||
{'email': 'email1'}
|
||||
{'email': 'email2'}
|
||||
{'email': 'email3'}
|
||||
]
|
||||
"""
|
||||
enrolled_inactive_user_emails = CourseEnrollment.objects.filter(
|
||||
course_id=course_key,
|
||||
is_active=True,
|
||||
user__is_active=False
|
||||
).annotate(
|
||||
email=F('user__email')
|
||||
).values('email')
|
||||
|
||||
def extract_student(student, features):
|
||||
"""
|
||||
Build dict containing information about a single inactive enrolled student.
|
||||
"""
|
||||
return {feature: student.get(feature, None) for feature in features}
|
||||
|
||||
return [extract_student(student, features) for student in enrolled_inactive_user_emails]
|
||||
|
||||
|
||||
def get_proctored_exam_results(course_key, features):
|
||||
"""
|
||||
Return info about proctored exam results in a course as a dict.
|
||||
|
||||
@@ -32,6 +32,7 @@ from lms.djangoapps.instructor_task.data import InstructorTaskTypes
|
||||
from lms.djangoapps.instructor_task.models import InstructorTask, InstructorTaskSchedule, SCHEDULED
|
||||
from lms.djangoapps.instructor_task.tasks import (
|
||||
calculate_grades_csv,
|
||||
calculate_inactive_enrolled_students_info_csv,
|
||||
calculate_may_enroll_csv,
|
||||
calculate_problem_grade_report,
|
||||
calculate_problem_responses_csv,
|
||||
@@ -409,6 +410,21 @@ def submit_calculate_may_enroll_csv(request, course_key, features):
|
||||
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_calculate_inactive_enrolled_students_csv(request, course_key, features):
|
||||
"""
|
||||
Submits a task to generate a CSV file containing information about
|
||||
enrolled students in a course who have not activated their account yet.
|
||||
|
||||
Raises AlreadyRunningError if said file is already being updated.
|
||||
"""
|
||||
task_type = InstructorTaskTypes.INACTIVE_ENROLLED_STUDENTS_INFO_CSV
|
||||
task_class = calculate_inactive_enrolled_students_info_csv
|
||||
task_input = {'features': features}
|
||||
task_key = ""
|
||||
|
||||
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_course_survey_report(request, course_key):
|
||||
"""
|
||||
Submits a task to generate a HTML File containing the executive summary report.
|
||||
|
||||
@@ -114,7 +114,7 @@ def generate_already_running_error_message(task_type):
|
||||
'proctored_exam_results_report': _('proctored exam results'),
|
||||
'export_ora2_data': _('ORA data'),
|
||||
'grade_course': _('grade'),
|
||||
|
||||
'inactive_enrolled_students_info_csv': _('inactive enrollment')
|
||||
}
|
||||
|
||||
if report_types.get(task_type):
|
||||
|
||||
@@ -24,6 +24,7 @@ class InstructorTaskTypes(str, Enum):
|
||||
GRADE_COURSE = "grade_course"
|
||||
GRADE_PROBLEMS = "grade_problems"
|
||||
MAY_ENROLL_INFO_CSV = "may_enroll_info_csv"
|
||||
INACTIVE_ENROLLED_STUDENTS_INFO_CSV = "inactive_enrolled_students_info_csv"
|
||||
OVERRIDE_PROBLEM_SCORE = "override_problem_score"
|
||||
PROBLEM_RESPONSES_CSV = "problem_responses_csv"
|
||||
PROCTORED_EXAM_RESULTS_REPORT = "proctored_exam_results_report"
|
||||
|
||||
@@ -30,7 +30,11 @@ from edx_django_utils.monitoring import set_code_owner_attribute
|
||||
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.enrollments import upload_may_enroll_csv, upload_students_csv
|
||||
from lms.djangoapps.instructor_task.tasks_helper.enrollments import (
|
||||
upload_inactive_enrolled_students_info_csv,
|
||||
upload_may_enroll_csv,
|
||||
upload_students_csv
|
||||
)
|
||||
from lms.djangoapps.instructor_task.tasks_helper.grades import CourseGradeReport, ProblemGradeReport, ProblemResponses
|
||||
from lms.djangoapps.instructor_task.tasks_helper.misc import (
|
||||
cohort_students_and_upload,
|
||||
@@ -267,6 +271,20 @@ def calculate_may_enroll_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_inactive_enrolled_students_info_csv(entry_id, xblock_instance_args):
|
||||
"""
|
||||
Compute information about invited students who have not enrolled
|
||||
in a given course yet 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_inactive_enrolled_students_info_csv, xblock_instance_args)
|
||||
return run_main_task(entry_id, task_fn, action_name)
|
||||
|
||||
|
||||
@shared_task(base=BaseInstructorTask)
|
||||
@set_code_owner_attribute
|
||||
def generate_certificates(entry_id, xblock_instance_args):
|
||||
|
||||
@@ -7,7 +7,11 @@ import logging
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
from pytz import UTC
|
||||
from lms.djangoapps.instructor_analytics.basic import enrolled_students_features, list_may_enroll
|
||||
from lms.djangoapps.instructor_analytics.basic import (
|
||||
enrolled_students_features,
|
||||
list_inactive_enrolled_students,
|
||||
list_may_enroll,
|
||||
)
|
||||
from lms.djangoapps.instructor_analytics.csvs import format_dictlist
|
||||
from common.djangoapps.student.models import CourseEnrollment # lint-amnesty, pylint: disable=unused-import
|
||||
|
||||
@@ -50,6 +54,38 @@ def upload_may_enroll_csv(_xblock_instance_args, _entry_id, course_id, task_inpu
|
||||
return task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
|
||||
def upload_inactive_enrolled_students_info_csv(_xblock_instance_args, _entry_id, course_id, task_input, action_name):
|
||||
"""
|
||||
For a given `course_id`, generate a CSV file containing
|
||||
information about students who are enrolled in a course but have not
|
||||
activated their account yet, and store using a `ReportStore`.
|
||||
"""
|
||||
start_time = time()
|
||||
start_date = datetime.now(UTC)
|
||||
num_reports = 1
|
||||
task_progress = TaskProgress(action_name, num_reports, start_time)
|
||||
current_step = {'step': 'Calculating info about students who are enrolled and their account is inactive'}
|
||||
task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
# Compute result table and format it
|
||||
query_features = task_input.get('features')
|
||||
student_data = list_inactive_enrolled_students(course_id, query_features)
|
||||
header, rows = format_dictlist(student_data, query_features)
|
||||
|
||||
task_progress.attempted = task_progress.succeeded = len(rows)
|
||||
task_progress.skipped = task_progress.total - task_progress.attempted
|
||||
|
||||
rows.insert(0, header)
|
||||
|
||||
current_step = {'step': 'Uploading CSV'}
|
||||
task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
# Perform the upload
|
||||
upload_csv_to_report_store(rows, 'inactive_enrolled_students_info', course_id, start_date)
|
||||
|
||||
return task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
|
||||
def upload_students_csv(_xblock_instance_args, _entry_id, course_id, task_input, action_name):
|
||||
"""
|
||||
For a given `course_id`, generate a CSV file containing profile
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
this.$proctored_exam_csv_btn = this.$section.find("input[name='proctored-exam-results-report']");
|
||||
this.$survey_results_csv_btn = this.$section.find("input[name='survey-results-report']");
|
||||
this.$list_may_enroll_csv_btn = this.$section.find("input[name='list-may-enroll-csv']");
|
||||
this.$list_learners_not_activated_csv_btn = this.$section.find("input[name='list-learners-not-activated-csv']");
|
||||
this.$list_problem_responses_csv_input = this.$section.find("input[name='problem-location']");
|
||||
this.$list_problem_responses_csv_btn = this.$section.find("input[name='list-problem-responses-csv']");
|
||||
this.$list_anon_btn = this.$section.find("input[name='list-anon-ids']");
|
||||
@@ -321,6 +322,31 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
this.$list_learners_not_activated_csv_btn.click(function() {
|
||||
var url = dataDownloadObj.$list_learners_not_activated_csv_btn.data('endpoint');
|
||||
var errorMessage = gettext('Error generating list of inactive students who are enrolled in a course. 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'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
this.$grade_config_btn.click(function() {
|
||||
var url = dataDownloadObj.$grade_config_btn.data('endpoint');
|
||||
return $.ajax({
|
||||
|
||||
@@ -37,6 +37,10 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
|
||||
<p><input type="button" name="list-may-enroll-csv" value="${_("Download a CSV of learners who can enroll")}" data-endpoint="${ section_data['get_students_who_may_enroll_url'] }" data-csv="true"></p>
|
||||
|
||||
<p>${_("Click to generate a CSV file that lists learners who are enrolled in the course but have not yet activated their account.")}</p>
|
||||
|
||||
<p><input type="button" name="list-learners-not-activated-csv" value="${_("Download a CSV of learners who have not activated their account")}" data-endpoint="${ section_data['get_inactive_enrolled_students_url'] }" data-csv="true"></p>
|
||||
|
||||
<p>${_("Click to download a CSV of anonymized student IDs:")}</p>
|
||||
<p><input type="button" name="list-anon-ids" value="${_("Get Student Anonymized IDs CSV")}" data-csv="true" class="csv" data-endpoint="${ section_data['get_anon_ids_url'] }" class="${'is-disabled' if disable_buttons else ''}" aria-disabled="${'true' if disable_buttons else 'false'}" ></p>
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
Learner
|
||||
who can enroll
|
||||
</option>
|
||||
<option value="inactiveEnrolledlearners"
|
||||
data-endpoint="${ section_data['get_inactive_enrolled_students_url'] }" data-csv="true">
|
||||
Enrolled Learner who has an inactive account
|
||||
</option>
|
||||
<option value="listEnrolledPeople"
|
||||
data-endpoint="${ section_data['get_students_features_url'] }"
|
||||
data-datatable="true">
|
||||
@@ -95,6 +99,9 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<p hidden="hidden" class="selectionInfo reports learnerWhoCanEnroll">${_("Click to generate a CSV file \
|
||||
that lists learners who can enroll in the course but have not yet done so.")}</p>
|
||||
|
||||
<p hidden="hidden" class="selectionInfo reports inactiveEnrolledlearners">${_("Click to generate a CSV file \
|
||||
that lists learners who are enrolled in the course but have not yet activated their account.")}</p>
|
||||
|
||||
<p hidden="hidden" class="selectionInfo reports proctoredExamResults">${_("Click to generate a CSV file \
|
||||
of all proctored exam results in this course.")}</p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user