TNL-1652: Allow instructors to obtain CSV file listing students who may
enroll in a course but have not signed up yet.
This commit is contained in:
@@ -22,7 +22,9 @@ from instructor_task.tasks import (
|
||||
calculate_problem_grade_report,
|
||||
calculate_students_features_csv,
|
||||
cohort_students,
|
||||
enrollment_report_features_csv)
|
||||
enrollment_report_features_csv,
|
||||
calculate_may_enroll_csv,
|
||||
)
|
||||
|
||||
from instructor_task.api_helper import (
|
||||
check_arguments_for_rescoring,
|
||||
@@ -375,6 +377,21 @@ def submit_detailed_enrollment_features_csv(request, course_key): # pylint: dis
|
||||
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
|
||||
invited students who have not enrolled in a given course yet.
|
||||
|
||||
Raises AlreadyRunningError if said file is already being updated.
|
||||
"""
|
||||
task_type = 'may_enroll_info_csv'
|
||||
task_class = calculate_may_enroll_csv
|
||||
task_input = {'features': features}
|
||||
task_key = ""
|
||||
|
||||
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_cohort_students(request, course_key, file_name):
|
||||
"""
|
||||
Request to have students cohorted in bulk.
|
||||
|
||||
@@ -38,7 +38,9 @@ from instructor_task.tasks_helper import (
|
||||
upload_problem_grade_report,
|
||||
upload_students_csv,
|
||||
cohort_students_and_upload,
|
||||
upload_enrollment_report)
|
||||
upload_enrollment_report,
|
||||
upload_may_enroll_csv,
|
||||
)
|
||||
|
||||
|
||||
TASK_LOG = logging.getLogger('edx.celery.task')
|
||||
@@ -197,6 +199,19 @@ def enrollment_report_features_csv(entry_id, xmodule_instance_args):
|
||||
return run_main_task(entry_id, task_fn, action_name)
|
||||
|
||||
|
||||
@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable
|
||||
def calculate_may_enroll_csv(entry_id, xmodule_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 = ugettext_noop('generated')
|
||||
task_fn = partial(upload_may_enroll_csv, xmodule_instance_args)
|
||||
return run_main_task(entry_id, task_fn, action_name)
|
||||
|
||||
|
||||
@task(base=BaseInstructorTask) # pylint: disable=E1102
|
||||
def cohort_students(entry_id, xmodule_instance_args):
|
||||
"""
|
||||
|
||||
@@ -32,7 +32,7 @@ from courseware.grades import iterate_grades_for
|
||||
from courseware.models import StudentModule
|
||||
from courseware.model_data import FieldDataCache
|
||||
from courseware.module_render import get_module_for_descriptor_internal
|
||||
from instructor_analytics.basic import enrolled_students_features
|
||||
from instructor_analytics.basic import enrolled_students_features, list_may_enroll
|
||||
from instructor_analytics.csvs import format_dictlist
|
||||
from instructor_task.models import ReportStore, InstructorTask, PROGRESS
|
||||
from lms.djangoapps.lms_xblock.runtime import LmsPartitionService
|
||||
@@ -991,6 +991,38 @@ def upload_enrollment_report(_xmodule_instance_args, _entry_id, course_id, _task
|
||||
return task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
|
||||
def upload_may_enroll_csv(_xmodule_instance_args, _entry_id, course_id, task_input, action_name):
|
||||
"""
|
||||
For a given `course_id`, generate a CSV file containing
|
||||
information about students who may enroll but have not done so
|
||||
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 may enroll'}
|
||||
task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
# Compute result table and format it
|
||||
query_features = task_input.get('features')
|
||||
student_data = list_may_enroll(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, 'may_enroll_info', course_id, start_date)
|
||||
|
||||
return task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
|
||||
def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, task_input, action_name):
|
||||
"""
|
||||
Within a given course, cohort students in bulk, then upload the results
|
||||
|
||||
@@ -16,7 +16,9 @@ from instructor_task.api import (
|
||||
submit_bulk_course_email,
|
||||
submit_calculate_students_features_csv,
|
||||
submit_cohort_students,
|
||||
submit_detailed_enrollment_features_csv)
|
||||
submit_detailed_enrollment_features_csv,
|
||||
submit_calculate_may_enroll_csv,
|
||||
)
|
||||
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
from instructor_task.models import InstructorTask, PROGRESS
|
||||
@@ -212,6 +214,14 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
|
||||
self.course.id)
|
||||
self._test_resubmission(api_call)
|
||||
|
||||
def test_submit_calculate_may_enroll(self):
|
||||
api_call = lambda: submit_calculate_may_enroll_csv(
|
||||
self.create_task_request(self.instructor),
|
||||
self.course.id,
|
||||
features=[]
|
||||
)
|
||||
self._test_resubmission(api_call)
|
||||
|
||||
def test_submit_cohort_students(self):
|
||||
api_call = lambda: submit_cohort_students(
|
||||
self.create_task_request(self.instructor),
|
||||
|
||||
@@ -27,13 +27,20 @@ from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartiti
|
||||
from shoppingcart.models import Order, PaidCourseRegistration, CourseRegistrationCode, Invoice, \
|
||||
CourseRegistrationCodeInvoiceItem, InvoiceTransaction
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED
|
||||
from student.models import (
|
||||
CourseEnrollment, CourseEnrollmentAllowed, ManualEnrollmentAudit,
|
||||
ALLOWEDTOENROLL_TO_ENROLLED
|
||||
)
|
||||
from verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
from instructor_task.models import ReportStore
|
||||
from instructor_task.tasks_helper import (
|
||||
cohort_students_and_upload, upload_grades_csv, upload_problem_grade_report, upload_students_csv
|
||||
cohort_students_and_upload,
|
||||
upload_grades_csv,
|
||||
upload_problem_grade_report,
|
||||
upload_students_csv,
|
||||
upload_may_enroll_csv,
|
||||
)
|
||||
from openedx.core.djangoapps.util.testing import ContentGroupTestCase, TestConditionalContent
|
||||
|
||||
@@ -753,6 +760,51 @@ class TestStudentReport(TestReportMixin, InstructorTaskCourseTestCase):
|
||||
self.assertDictContainsSubset({'attempted': num_students, 'succeeded': num_students, 'failed': 0}, result)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestListMayEnroll(TestReportMixin, InstructorTaskCourseTestCase):
|
||||
"""
|
||||
Tests that generation of CSV files containing information about
|
||||
students who may enroll in a given course (but have not signed up
|
||||
for it yet) works.
|
||||
"""
|
||||
def _create_enrollment(self, email):
|
||||
"Factory method for creating CourseEnrollmentAllowed objects."
|
||||
return CourseEnrollmentAllowed.objects.create(
|
||||
email=email, course_id=self.course.id
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(TestListMayEnroll, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
|
||||
def test_success(self):
|
||||
self._create_enrollment('user@example.com')
|
||||
task_input = {'features': []}
|
||||
with patch('instructor_task.tasks_helper._get_current_task'):
|
||||
result = upload_may_enroll_csv(None, None, self.course.id, task_input, 'calculated')
|
||||
report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD')
|
||||
links = report_store.links_for(self.course.id)
|
||||
|
||||
self.assertEquals(len(links), 1)
|
||||
self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result)
|
||||
|
||||
def test_unicode_email_addresses(self):
|
||||
"""
|
||||
Test handling of unicode characters in email addresses of students
|
||||
who may enroll in a course.
|
||||
"""
|
||||
enrollments = [u'student@example.com', u'ni\xf1o@example.com']
|
||||
for email in enrollments:
|
||||
self._create_enrollment(email)
|
||||
|
||||
task_input = {'features': ['email']}
|
||||
with patch('instructor_task.tasks_helper._get_current_task'):
|
||||
result = upload_may_enroll_csv(None, None, self.course.id, task_input, 'calculated')
|
||||
# This assertion simply confirms that the generation completed with no errors
|
||||
num_enrollments = len(enrollments)
|
||||
self.assertDictContainsSubset({'attempted': num_enrollments, 'succeeded': num_enrollments, 'failed': 0}, result)
|
||||
|
||||
|
||||
class MockDefaultStorage(object):
|
||||
"""Mock django's DefaultStorage"""
|
||||
def __init__(self):
|
||||
|
||||
Reference in New Issue
Block a user