From f711a32e3da0731cb8fce5700c4b99e31226a3b6 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Mon, 18 May 2015 14:38:52 +0200 Subject: [PATCH] TNL-1652: Allow instructors to obtain CSV file listing students who may enroll in a course but have not signed up yet. --- common/djangoapps/student/models.py | 13 +++++ lms/djangoapps/instructor/tests/test_api.py | 37 ++++++++++++ lms/djangoapps/instructor/views/api.py | 30 ++++++++++ lms/djangoapps/instructor/views/api_urls.py | 2 + .../instructor/views/instructor_dashboard.py | 3 + lms/djangoapps/instructor/views/legacy.py | 6 -- lms/djangoapps/instructor_analytics/basic.py | 26 +++++++++ .../instructor_analytics/tests/test_basic.py | 20 ++++++- lms/djangoapps/instructor_task/api.py | 19 ++++++- lms/djangoapps/instructor_task/tasks.py | 17 +++++- .../instructor_task/tasks_helper.py | 34 ++++++++++- .../instructor_task/tests/test_api.py | 12 +++- .../tests/test_tasks_helper.py | 56 ++++++++++++++++++- .../instructor_dashboard/data_download.coffee | 15 +++++ .../legacy_instructor_dashboard.html | 10 +++- .../instructor_dashboard_2/data_download.html | 4 ++ 16 files changed, 287 insertions(+), 17 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 456796ce9f..3990ab1846 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -1380,6 +1380,19 @@ class CourseEnrollmentAllowed(models.Model): def __unicode__(self): 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 class CourseAccessRole(models.Model): diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 8c79173692..5bbd7d1bfd 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -96,6 +96,12 @@ REPORTS_DATA = ( 'instructor_api_endpoint': 'get_enrollment_report', 'task_api_endpoint': 'instructor_task.api.submit_detailed_enrollment_features_csv', '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): ('calculate_grades_csv', {}), ('get_students_features', {}), ('get_enrollment_report', {}), + ('get_students_who_may_enroll', {}), ] # Endpoints that only Instructors can access self.instructor_level_endpoints = [ @@ -1977,6 +1984,12 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa for student in self.students: 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): """ enroll user using a registration code @@ -2271,6 +2284,30 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa 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): """ Test assert require_course fiance_admin before generating diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 0e286a8722..d742809fc7 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1112,6 +1112,36 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red return JsonResponse({"status": already_running_status}) +@ensure_csrf_cookie +@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 diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index a12a38bcc9..e3a55a33b7 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -21,6 +21,8 @@ urlpatterns = patterns( 'instructor.views.api.get_grading_config', name="get_grading_config"), url(r'^get_students_features(?P/csv)?$', '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$', 'instructor.views.api.get_user_invoice_preference', name="get_user_invoice_preference"), url(r'^get_sale_records(?P/csv)?$', diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 9211371526..be24f588c5 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -445,6 +445,9 @@ def _section_data_download(course, access): 'access': access, '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_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)}), '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)}), diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 037fe0e07c..951291f925 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -307,12 +307,6 @@ def instructor_dashboard(request, course_id): #---------------------------------------- # 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': is_shib_course = uses_shib(course) diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py index 1413118fdb..6bda306bbd 100644 --- a/lms/djangoapps/instructor_analytics/basic.py +++ b/lms/djangoapps/instructor_analytics/basic.py @@ -15,6 +15,7 @@ from django.core.urlresolvers import reverse import xmodule.graders as xmgraders from django.core.exceptions import ObjectDoesNotExist from microsite_configuration import microsite +from student.models import CourseEnrollmentAllowed STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'email') @@ -209,6 +210,31 @@ def enrolled_students_features(course_key, features): 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): """ Return list of Coupon Codes as dictionaries. diff --git a/lms/djangoapps/instructor_analytics/tests/test_basic.py b/lms/djangoapps/instructor_analytics/tests/test_basic.py index e40144b06b..087219eb2a 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_basic.py +++ b/lms/djangoapps/instructor_analytics/tests/test_basic.py @@ -3,7 +3,7 @@ Tests for instructor.basic """ import json -from student.models import CourseEnrollment +from student.models import CourseEnrollment, CourseEnrollmentAllowed from django.core.urlresolvers import reverse from mock import patch from student.roles import CourseSalesAdminRole @@ -14,8 +14,9 @@ from shoppingcart.models import ( ) from course_modes.models import CourseMode from instructor_analytics.basic import ( - sale_record_features, sale_order_record_features, enrolled_students_features, course_registration_features, - coupon_codes_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES + sale_record_features, sale_order_record_features, enrolled_students_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 courseware.tests.factories import InstructorFactory @@ -43,6 +44,11 @@ class TestAnalyticsBasic(ModuleStoreTestCase): "company": "Open edX Inc {}".format(user.id), }) 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): self.assertIn('username', AVAILABLE_FEATURES) @@ -113,6 +119,14 @@ class TestAnalyticsBasic(ModuleStoreTestCase): self.assertEqual(len(AVAILABLE_FEATURES), len(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}) class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase): diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py index 35c81bee69..2c0b7b0551 100644 --- a/lms/djangoapps/instructor_task/api.py +++ b/lms/djangoapps/instructor_task/api.py @@ -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. diff --git a/lms/djangoapps/instructor_task/tasks.py b/lms/djangoapps/instructor_task/tasks.py index 2b81c9463d..02bdcde464 100644 --- a/lms/djangoapps/instructor_task/tasks.py +++ b/lms/djangoapps/instructor_task/tasks.py @@ -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): """ diff --git a/lms/djangoapps/instructor_task/tasks_helper.py b/lms/djangoapps/instructor_task/tasks_helper.py index 3dc062fa85..62917cd8a9 100644 --- a/lms/djangoapps/instructor_task/tasks_helper.py +++ b/lms/djangoapps/instructor_task/tasks_helper.py @@ -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 diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py index 5e9a0b757a..2dab82351d 100644 --- a/lms/djangoapps/instructor_task/tests/test_api.py +++ b/lms/djangoapps/instructor_task/tests/test_api.py @@ -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), diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index fa6beedf64..2e4deda8a5 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -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): diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee index 548222fa50..4fa8de081e 100644 --- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee +++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee @@ -20,6 +20,7 @@ class DataDownload # gather elements @$list_studs_btn = @$section.find("input[name='list-profiles']'") @$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']'") @$grade_config_btn = @$section.find("input[name='dump-gradeconf']'") @$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'") @@ -96,6 +97,20 @@ class DataDownload grid = new Slick.Grid($table_placeholder, grid_data, columns, options) # 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) => url = @$grade_config_btn.data 'endpoint' # display html from grading config endpoint diff --git a/lms/templates/courseware/legacy_instructor_dashboard.html b/lms/templates/courseware/legacy_instructor_dashboard.html index c5becea045..7cf15e4416 100644 --- a/lms/templates/courseware/legacy_instructor_dashboard.html +++ b/lms/templates/courseware/legacy_instructor_dashboard.html @@ -330,8 +330,14 @@ function goto( mode) % endif - - +

+ ${_("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.")} +

+ +

+ ${_("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.")} +

+
%if settings.FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html index 28923c01e2..a7110098fa 100644 --- a/lms/templates/instructor/instructor_dashboard_2/data_download.html +++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html @@ -31,6 +31,10 @@

+

${_("Click to generate a CSV file that lists learners who can enroll in the course but have not yet done so.")}

+ +

+ % if not disable_buttons:

${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}