Commit f711a32e by Tim Krones

TNL-1652: Allow instructors to obtain CSV file listing students who may

enroll in a course but have not signed up yet.
parent 298ba727
...@@ -1380,6 +1380,19 @@ class CourseEnrollmentAllowed(models.Model): ...@@ -1380,6 +1380,19 @@ class CourseEnrollmentAllowed(models.Model):
def __unicode__(self): def __unicode__(self):
return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created)
@classmethod
def may_enroll_and_unenrolled(cls, course_id):
"""
Return QuerySet of students who are allowed to enroll in a course.
Result excludes students who have already enrolled in the
course.
`course_id` identifies the course for which to compute the QuerySet.
"""
enrolled = CourseEnrollment.objects.users_enrolled_in(course_id=course_id).values_list('email', flat=True)
return CourseEnrollmentAllowed.objects.filter(course_id=course_id).exclude(email__in=enrolled)
@total_ordering @total_ordering
class CourseAccessRole(models.Model): class CourseAccessRole(models.Model):
......
...@@ -96,6 +96,12 @@ REPORTS_DATA = ( ...@@ -96,6 +96,12 @@ REPORTS_DATA = (
'instructor_api_endpoint': 'get_enrollment_report', 'instructor_api_endpoint': 'get_enrollment_report',
'task_api_endpoint': 'instructor_task.api.submit_detailed_enrollment_features_csv', 'task_api_endpoint': 'instructor_task.api.submit_detailed_enrollment_features_csv',
'extra_instructor_api_kwargs': {} 'extra_instructor_api_kwargs': {}
},
{
'report_type': 'students who may enroll',
'instructor_api_endpoint': 'get_students_who_may_enroll',
'task_api_endpoint': 'instructor_task.api.submit_calculate_may_enroll_csv',
'extra_instructor_api_kwargs': {},
} }
) )
...@@ -208,6 +214,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -208,6 +214,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
('calculate_grades_csv', {}), ('calculate_grades_csv', {}),
('get_students_features', {}), ('get_students_features', {}),
('get_enrollment_report', {}), ('get_enrollment_report', {}),
('get_students_who_may_enroll', {}),
] ]
# Endpoints that only Instructors can access # Endpoints that only Instructors can access
self.instructor_level_endpoints = [ self.instructor_level_endpoints = [
...@@ -1977,6 +1984,12 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -1977,6 +1984,12 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for student in self.students: for student in self.students:
CourseEnrollment.enroll(student, self.course.id) CourseEnrollment.enroll(student, self.course.id)
self.students_who_may_enroll = self.students + [UserFactory() for _ in range(5)]
for student in self.students_who_may_enroll:
CourseEnrollmentAllowed.objects.create(
email=student.email, course_id=self.course.id
)
def register_with_redemption_code(self, user, code): def register_with_redemption_code(self, user, code):
""" """
enroll user using a registration code enroll user using a registration code
...@@ -2271,6 +2284,30 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -2271,6 +2284,30 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
self.assertEqual('cohort' in res_json['feature_names'], is_cohorted) self.assertEqual('cohort' in res_json['feature_names'], is_cohorted)
def test_get_students_who_may_enroll(self):
"""
Test whether get_students_who_may_enroll returns an appropriate
status message when users request a CSV file of students who
may enroll in a course.
"""
url = reverse(
'get_students_who_may_enroll',
kwargs={'course_id': unicode(self.course.id)}
)
# Successful case:
response = self.client.get(url, {})
res_json = json.loads(response.content)
self.assertIn('status', res_json)
self.assertNotIn('already in progress', res_json['status'])
# CSV generation already in progress:
with patch('instructor_task.api.submit_calculate_may_enroll_csv') as submit_task_function:
error = AlreadyRunningError()
submit_task_function.side_effect = error
response = self.client.get(url, {})
res_json = json.loads(response.content)
self.assertIn('status', res_json)
self.assertIn('already in progress', res_json['status'])
def test_access_course_finance_admin_with_invalid_course_key(self): def test_access_course_finance_admin_with_invalid_course_key(self):
""" """
Test assert require_course fiance_admin before generating Test assert require_course fiance_admin before generating
......
...@@ -1114,6 +1114,36 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red ...@@ -1114,6 +1114,36 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def get_students_who_may_enroll(request, course_id):
"""
Initiate generation of a CSV file containing information about
students who may enroll in a course.
Responds with JSON
{"status": "... status message ..."}
"""
course_key = CourseKey.from_string(course_id)
query_features = ['email']
try:
instructor_task.api.submit_calculate_may_enroll_csv(request, course_key, query_features)
success_status = _(
"Your students who may enroll report is being generated! "
"You can view the status of the generation task in the 'Pending Instructor Tasks' section."
)
return JsonResponse({"status": success_status})
except AlreadyRunningError:
already_running_status = _(
"A students who may enroll report generation task is already in progress. "
"Check the 'Pending Instructor Tasks' table for the status of the task. "
"When completed, the report will be available for download in the table below."
)
return JsonResponse({"status": already_running_status})
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_POST @require_POST
@require_level('staff') @require_level('staff')
def add_users_to_cohorts(request, course_id): def add_users_to_cohorts(request, course_id):
......
...@@ -21,6 +21,8 @@ urlpatterns = patterns( ...@@ -21,6 +21,8 @@ urlpatterns = patterns(
'instructor.views.api.get_grading_config', name="get_grading_config"), 'instructor.views.api.get_grading_config', name="get_grading_config"),
url(r'^get_students_features(?P<csv>/csv)?$', url(r'^get_students_features(?P<csv>/csv)?$',
'instructor.views.api.get_students_features', name="get_students_features"), 'instructor.views.api.get_students_features', name="get_students_features"),
url(r'^get_students_who_may_enroll$',
'instructor.views.api.get_students_who_may_enroll', name="get_students_who_may_enroll"),
url(r'^get_user_invoice_preference$', url(r'^get_user_invoice_preference$',
'instructor.views.api.get_user_invoice_preference', name="get_user_invoice_preference"), 'instructor.views.api.get_user_invoice_preference', name="get_user_invoice_preference"),
url(r'^get_sale_records(?P<csv>/csv)?$', url(r'^get_sale_records(?P<csv>/csv)?$',
......
...@@ -445,6 +445,9 @@ def _section_data_download(course, access): ...@@ -445,6 +445,9 @@ def _section_data_download(course, access):
'access': access, 'access': access,
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': unicode(course_key)}), 'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': unicode(course_key)}),
'get_students_features_url': reverse('get_students_features', kwargs={'course_id': unicode(course_key)}), 'get_students_features_url': reverse('get_students_features', kwargs={'course_id': unicode(course_key)}),
'get_students_who_may_enroll_url': reverse(
'get_students_who_may_enroll', kwargs={'course_id': unicode(course_key)}
),
'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': unicode(course_key)}), 'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': unicode(course_key)}),
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}), 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}),
'list_report_downloads_url': reverse('list_report_downloads', kwargs={'course_id': unicode(course_key)}), 'list_report_downloads_url': reverse('list_report_downloads', kwargs={'course_id': unicode(course_key)}),
......
...@@ -307,12 +307,6 @@ def instructor_dashboard(request, course_id): ...@@ -307,12 +307,6 @@ def instructor_dashboard(request, course_id):
#---------------------------------------- #----------------------------------------
# enrollment # enrollment
elif action == 'List students who may enroll but may not have yet signed up':
ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key)
datatable = {'header': ['StudentEmail']}
datatable['data'] = [[x.email] for x in ceaset]
datatable['title'] = action
elif action == 'Enroll multiple students': elif action == 'Enroll multiple students':
is_shib_course = uses_shib(course) is_shib_course = uses_shib(course)
......
...@@ -15,6 +15,7 @@ from django.core.urlresolvers import reverse ...@@ -15,6 +15,7 @@ from django.core.urlresolvers import reverse
import xmodule.graders as xmgraders import xmodule.graders as xmgraders
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from microsite_configuration import microsite from microsite_configuration import microsite
from student.models import CourseEnrollmentAllowed
STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'email') STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'email')
...@@ -209,6 +210,31 @@ def enrolled_students_features(course_key, features): ...@@ -209,6 +210,31 @@ def enrolled_students_features(course_key, features):
return [extract_student(student, features) for student in students] return [extract_student(student, features) for student in students]
def list_may_enroll(course_key, features):
"""
Return info about students who may enroll in a course as a dict.
list_may_enroll(course_key, ['email'])
would return [
{'email': 'email1'}
{'email': 'email2'}
{'email': 'email3'}
]
Note that result does not include students who may enroll and have
already done so.
"""
may_enroll_and_unenrolled = CourseEnrollmentAllowed.may_enroll_and_unenrolled(course_key)
def extract_student(student, features):
"""
Build dict containing information about a single student.
"""
return dict((feature, getattr(student, feature)) for feature in features)
return [extract_student(student, features) for student in may_enroll_and_unenrolled]
def coupon_codes_features(features, coupons_list): def coupon_codes_features(features, coupons_list):
""" """
Return list of Coupon Codes as dictionaries. Return list of Coupon Codes as dictionaries.
......
...@@ -3,7 +3,7 @@ Tests for instructor.basic ...@@ -3,7 +3,7 @@ Tests for instructor.basic
""" """
import json import json
from student.models import CourseEnrollment from student.models import CourseEnrollment, CourseEnrollmentAllowed
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from mock import patch from mock import patch
from student.roles import CourseSalesAdminRole from student.roles import CourseSalesAdminRole
...@@ -14,8 +14,9 @@ from shoppingcart.models import ( ...@@ -14,8 +14,9 @@ from shoppingcart.models import (
) )
from course_modes.models import CourseMode from course_modes.models import CourseMode
from instructor_analytics.basic import ( from instructor_analytics.basic import (
sale_record_features, sale_order_record_features, enrolled_students_features, course_registration_features, sale_record_features, sale_order_record_features, enrolled_students_features,
coupon_codes_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES course_registration_features, coupon_codes_features, list_may_enroll,
AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
) )
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from courseware.tests.factories import InstructorFactory from courseware.tests.factories import InstructorFactory
...@@ -43,6 +44,11 @@ class TestAnalyticsBasic(ModuleStoreTestCase): ...@@ -43,6 +44,11 @@ class TestAnalyticsBasic(ModuleStoreTestCase):
"company": "Open edX Inc {}".format(user.id), "company": "Open edX Inc {}".format(user.id),
}) })
user.profile.save() user.profile.save()
self.students_who_may_enroll = list(self.users) + [UserFactory() for _ in range(5)]
for student in self.students_who_may_enroll:
CourseEnrollmentAllowed.objects.create(
email=student.email, course_id=self.course_key
)
def test_enrolled_students_features_username(self): def test_enrolled_students_features_username(self):
self.assertIn('username', AVAILABLE_FEATURES) self.assertIn('username', AVAILABLE_FEATURES)
...@@ -113,6 +119,14 @@ class TestAnalyticsBasic(ModuleStoreTestCase): ...@@ -113,6 +119,14 @@ class TestAnalyticsBasic(ModuleStoreTestCase):
self.assertEqual(len(AVAILABLE_FEATURES), len(STUDENT_FEATURES + PROFILE_FEATURES)) self.assertEqual(len(AVAILABLE_FEATURES), len(STUDENT_FEATURES + PROFILE_FEATURES))
self.assertEqual(set(AVAILABLE_FEATURES), set(STUDENT_FEATURES + PROFILE_FEATURES)) self.assertEqual(set(AVAILABLE_FEATURES), set(STUDENT_FEATURES + PROFILE_FEATURES))
def test_list_may_enroll(self):
may_enroll = list_may_enroll(self.course_key, ['email'])
self.assertEqual(len(may_enroll), len(self.students_who_may_enroll) - len(self.users))
email_adresses = [student.email for student in self.students_who_may_enroll]
for student in may_enroll:
self.assertEqual(student.keys(), ['email'])
self.assertIn(student['email'], email_adresses)
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase): class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
......
...@@ -22,7 +22,9 @@ from instructor_task.tasks import ( ...@@ -22,7 +22,9 @@ from instructor_task.tasks import (
calculate_problem_grade_report, calculate_problem_grade_report,
calculate_students_features_csv, calculate_students_features_csv,
cohort_students, cohort_students,
enrollment_report_features_csv) enrollment_report_features_csv,
calculate_may_enroll_csv,
)
from instructor_task.api_helper import ( from instructor_task.api_helper import (
check_arguments_for_rescoring, check_arguments_for_rescoring,
...@@ -375,6 +377,21 @@ def submit_detailed_enrollment_features_csv(request, course_key): # pylint: dis ...@@ -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) 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): def submit_cohort_students(request, course_key, file_name):
""" """
Request to have students cohorted in bulk. Request to have students cohorted in bulk.
......
...@@ -38,7 +38,9 @@ from instructor_task.tasks_helper import ( ...@@ -38,7 +38,9 @@ from instructor_task.tasks_helper import (
upload_problem_grade_report, upload_problem_grade_report,
upload_students_csv, upload_students_csv,
cohort_students_and_upload, cohort_students_and_upload,
upload_enrollment_report) upload_enrollment_report,
upload_may_enroll_csv,
)
TASK_LOG = logging.getLogger('edx.celery.task') TASK_LOG = logging.getLogger('edx.celery.task')
...@@ -197,6 +199,19 @@ def enrollment_report_features_csv(entry_id, xmodule_instance_args): ...@@ -197,6 +199,19 @@ def enrollment_report_features_csv(entry_id, xmodule_instance_args):
return run_main_task(entry_id, task_fn, action_name) 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 @task(base=BaseInstructorTask) # pylint: disable=E1102
def cohort_students(entry_id, xmodule_instance_args): def cohort_students(entry_id, xmodule_instance_args):
""" """
......
...@@ -32,7 +32,7 @@ from courseware.grades import iterate_grades_for ...@@ -32,7 +32,7 @@ from courseware.grades import iterate_grades_for
from courseware.models import StudentModule from courseware.models import StudentModule
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor_internal 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_analytics.csvs import format_dictlist
from instructor_task.models import ReportStore, InstructorTask, PROGRESS from instructor_task.models import ReportStore, InstructorTask, PROGRESS
from lms.djangoapps.lms_xblock.runtime import LmsPartitionService from lms.djangoapps.lms_xblock.runtime import LmsPartitionService
...@@ -991,6 +991,38 @@ def upload_enrollment_report(_xmodule_instance_args, _entry_id, course_id, _task ...@@ -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) 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): 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 Within a given course, cohort students in bulk, then upload the results
......
...@@ -16,7 +16,9 @@ from instructor_task.api import ( ...@@ -16,7 +16,9 @@ from instructor_task.api import (
submit_bulk_course_email, submit_bulk_course_email,
submit_calculate_students_features_csv, submit_calculate_students_features_csv,
submit_cohort_students, 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.api_helper import AlreadyRunningError
from instructor_task.models import InstructorTask, PROGRESS from instructor_task.models import InstructorTask, PROGRESS
...@@ -212,6 +214,14 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa ...@@ -212,6 +214,14 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
self.course.id) self.course.id)
self._test_resubmission(api_call) 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): def test_submit_cohort_students(self):
api_call = lambda: submit_cohort_students( api_call = lambda: submit_cohort_students(
self.create_task_request(self.instructor), self.create_task_request(self.instructor),
......
...@@ -27,13 +27,20 @@ from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartiti ...@@ -27,13 +27,20 @@ from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartiti
from shoppingcart.models import Order, PaidCourseRegistration, CourseRegistrationCode, Invoice, \ from shoppingcart.models import Order, PaidCourseRegistration, CourseRegistrationCode, Invoice, \
CourseRegistrationCodeInvoiceItem, InvoiceTransaction CourseRegistrationCodeInvoiceItem, InvoiceTransaction
from student.tests.factories import UserFactory 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 verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.partitions.partitions import Group, UserPartition from xmodule.partitions.partitions import Group, UserPartition
from instructor_task.models import ReportStore from instructor_task.models import ReportStore
from instructor_task.tasks_helper import ( 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 from openedx.core.djangoapps.util.testing import ContentGroupTestCase, TestConditionalContent
...@@ -753,6 +760,51 @@ class TestStudentReport(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -753,6 +760,51 @@ class TestStudentReport(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'attempted': num_students, 'succeeded': num_students, 'failed': 0}, result) 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): class MockDefaultStorage(object):
"""Mock django's DefaultStorage""" """Mock django's DefaultStorage"""
def __init__(self): def __init__(self):
......
...@@ -20,6 +20,7 @@ class DataDownload ...@@ -20,6 +20,7 @@ class DataDownload
# gather elements # gather elements
@$list_studs_btn = @$section.find("input[name='list-profiles']'") @$list_studs_btn = @$section.find("input[name='list-profiles']'")
@$list_studs_csv_btn = @$section.find("input[name='list-profiles-csv']'") @$list_studs_csv_btn = @$section.find("input[name='list-profiles-csv']'")
@$list_may_enroll_csv_btn = @$section.find("input[name='list-may-enroll-csv']")
@$list_anon_btn = @$section.find("input[name='list-anon-ids']'") @$list_anon_btn = @$section.find("input[name='list-anon-ids']'")
@$grade_config_btn = @$section.find("input[name='dump-gradeconf']'") @$grade_config_btn = @$section.find("input[name='dump-gradeconf']'")
@$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'") @$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'")
...@@ -96,6 +97,20 @@ class DataDownload ...@@ -96,6 +97,20 @@ class DataDownload
grid = new Slick.Grid($table_placeholder, grid_data, columns, options) grid = new Slick.Grid($table_placeholder, grid_data, columns, options)
# grid.autosizeColumns() # grid.autosizeColumns()
@$list_may_enroll_csv_btn.click (e) =>
@clear_display()
url = @$list_may_enroll_csv_btn.data 'endpoint'
$.ajax
dataType: 'json'
url: url
error: (std_ajax_err) =>
@$reports_request_response_error.text gettext("Error generating list of students who may enroll. Please try again.")
$(".msg-error").css({"display":"block"})
success: (data) =>
@$reports_request_response.text data['status']
$(".msg-confirm").css({"display":"block"})
@$grade_config_btn.click (e) => @$grade_config_btn.click (e) =>
url = @$grade_config_btn.data 'endpoint' url = @$grade_config_btn.data 'endpoint'
# display html from grading config endpoint # display html from grading config endpoint
......
...@@ -330,8 +330,14 @@ function goto( mode) ...@@ -330,8 +330,14 @@ function goto( mode)
</div> </div>
% endif % endif
<input type="submit" name="action" value="List enrolled students" class="${'is-disabled' if disable_buttons else ''}" aria-disabled="${'true' if disable_buttons else 'false'}"> <p class="is-deprecated">
<input type="submit" name="action" value="List students who may enroll but may not have yet signed up" class="${'is-disabled' if disable_buttons else ''}" aria-disabled="${'true' if disable_buttons else 'false'}" > ${_("To download a CSV file containing profile information for students who are enrolled in this course, visit the Data Download section of the Instructor Dashboard.")}
</p>
<p class="is-deprecated">
${_("To download a list of students who may enroll in this course but have not yet signed up for it, visit the Data Download section of the Instructor Dashboard.")}
</p>
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
%if settings.FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: %if settings.FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access:
......
...@@ -31,6 +31,10 @@ ...@@ -31,6 +31,10 @@
<p><input type="button" name="list-profiles-csv" value="${_("Download profile information as a CSV")}" data-endpoint="${ section_data['get_students_features_url'] }" data-csv="true"></p> <p><input type="button" name="list-profiles-csv" value="${_("Download profile information as a CSV")}" data-endpoint="${ section_data['get_students_features_url'] }" data-csv="true"></p>
<p>${_("Click to generate a CSV file that lists learners who can enroll in the course but have not yet done so.")}</p>
<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>
% if not disable_buttons: % if not disable_buttons:
<p>${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}</p> <p>${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}</p>
<p><input type="button" name="list-profiles" value="${_("List enrolled students' profile information")}" data-endpoint="${ section_data['get_students_features_url'] }"></p> <p><input type="button" name="list-profiles" value="${_("List enrolled students' profile information")}" data-endpoint="${ section_data['get_students_features_url'] }"></p>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment