diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index e4bdcd4554ca6fbf7f815241e15f115ade54b17d..870e9e3ac38b8564c1eca668f93192b08b302e30 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -180,6 +180,7 @@ INSTRUCTOR_POST_ENDPOINTS = set([ 'get_problem_responses', 'get_proctored_exam_results', 'get_registration_codes', + 'get_student_enrollment_status', 'get_student_progress_url', 'get_students_features', 'get_students_who_may_enroll', @@ -1911,6 +1912,65 @@ class TestInstructorAPIEnrollment(SharedModuleStoreTestCase, LoginEnrollmentTest self.assertEqual(response.status_code, 200) return response + def test_get_enrollment_status(self): + """Check that enrollment states are reported correctly.""" + + # enrolled, active + url = reverse( + 'get_student_enrollment_status', + kwargs={'course_id': self.course.id.to_deprecated_string()}, + ) + params = { + 'unique_student_identifier': 'EnrolledStudent' + } + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + res_json = json.loads(response.content) + self.assertEqual( + res_json['enrollment_status'], + 'Enrollment status for EnrolledStudent: active' + ) + + # unenrolled, inactive + CourseEnrollment.unenroll( + self.enrolled_student, + self.course.id + ) + + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + res_json = json.loads(response.content) + self.assertEqual( + res_json['enrollment_status'], + 'Enrollment status for EnrolledStudent: inactive' + ) + + # invited, not yet registered + params = { + 'unique_student_identifier': 'robot-allowed@robot.org' + } + + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + res_json = json.loads(response.content) + self.assertEqual( + res_json['enrollment_status'], + 'Enrollment status for robot-allowed@robot.org: pending' + ) + + # never enrolled or invited + params = { + 'unique_student_identifier': 'nonotever@example.com' + } + + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + res_json = json.loads(response.content) + self.assertEqual( + res_json['enrollment_status'], + 'Enrollment status for nonotever@example.com: never enrolled' + ) + @attr(shard=5) @ddt.ddt diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index b2c50c4036688a9bd564a2018f9239ab941e5c4c..4493b10282e44cec83f9c57f7b972910ce1a73da 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -104,6 +104,7 @@ from student.models import ( UNENROLLED_TO_ENROLLED, UNENROLLED_TO_UNENROLLED, CourseEnrollment, + CourseEnrollmentAllowed, EntranceExamConfiguration, ManualEnrollmentAudit, Registration, @@ -1870,6 +1871,63 @@ def get_anon_ids(request, course_id): # pylint: disable=unused-argument @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) @require_level('staff') +@require_post_params( + unique_student_identifier="email or username of student for whom to get enrollment status" +) +def get_student_enrollment_status(request, course_id): + """ + Get the enrollment status of a student. + Limited to staff access. + + Takes query parameter unique_student_identifier + """ + + error = '' + user = None + mode = None + is_active = None + + course_id = CourseKey.from_string(course_id) + unique_student_identifier = request.POST.get('unique_student_identifier') + + try: + user = get_student_from_identifier(unique_student_identifier) + mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_id) + except User.DoesNotExist: + # The student could have been invited to enroll without having + # registered. We'll also look at CourseEnrollmentAllowed + # records, so let the lack of a User slide. + pass + + enrollment_status = _('Enrollment status for {student}: unknown').format(student=unique_student_identifier) + + if user and mode: + if is_active: + enrollment_status = _('Enrollment status for {student}: active').format(student=user) + else: + enrollment_status = _('Enrollment status for {student}: inactive').format(student=user) + else: + email = user.email if user else unique_student_identifier + allowed = CourseEnrollmentAllowed.may_enroll_and_unenrolled(course_id) + if allowed and email in [cea.email for cea in allowed]: + enrollment_status = _('Enrollment status for {student}: pending').format(student=email) + else: + enrollment_status = _('Enrollment status for {student}: never enrolled').format(student=email) + + response_payload = { + 'course_id': course_id.to_deprecated_string(), + 'error': error, + 'enrollment_status': enrollment_status + } + + return JsonResponse(response_payload) + + +@require_POST +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@common_exceptions_400 +@require_level('staff') @require_post_params( unique_student_identifier="email or username of student for whom to get progress url" ) diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 5272d7a4a9af46511121d0beda7c40345a6089d4..fb9b7a208ab40f351c0727d9acd802183b85dddc 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -22,6 +22,7 @@ urlpatterns = [ url(r'^get_sale_order_records$', api.get_sale_order_records, name='get_sale_order_records'), url(r'^sale_validation_url$', api.sale_validation, name='sale_validation'), url(r'^get_anon_ids$', api.get_anon_ids, name='get_anon_ids'), + url(r'^get_student_enrollment_status$', api.get_student_enrollment_status, name="get_student_enrollment_status"), url(r'^get_student_progress_url$', api.get_student_progress_url, name='get_student_progress_url'), url(r'^reset_student_attempts$', api.reset_student_attempts, name='reset_student_attempts'), url(r'^rescore_problem$', api.rescore_problem, name='rescore_problem'), diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 23ef4c32d892239cccdc2b89253837a8f8cb04f1..2d51870c8c301e39d8d5080ab76c47c9c3a4cdbb 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -565,6 +565,10 @@ def _section_student_admin(course, access): 'section_display_name': _('Student Admin'), 'access': access, 'is_small_course': is_small_course, + 'get_student_enrollment_status_url': reverse( + 'get_student_enrollment_status', + kwargs={'course_id': unicode(course_key)} + ), 'get_student_progress_url_url': reverse('get_student_progress_url', kwargs={'course_id': unicode(course_key)}), 'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': unicode(course_key)}), 'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': unicode(course_key)}), diff --git a/lms/static/js/instructor_dashboard/student_admin.js b/lms/static/js/instructor_dashboard/student_admin.js index 01ee7afe064ad4a1ead190afd14993498a7b8dbd..083475f879a281d90fbb4d121254be83da2c1973 100644 --- a/lms/static/js/instructor_dashboard/student_admin.js +++ b/lms/static/js/instructor_dashboard/student_admin.js @@ -32,6 +32,7 @@ var studentadmin = this; this.$section = $section; this.$section.data('wrapper', this); + this.$field_student_select_enrollment_status = findAndAssert(this.$section, "input[name='student-select-enrollment-status']"); this.$field_student_select_progress = findAndAssert(this.$section, "input[name='student-select-progress']"); this.$field_student_select_grade = findAndAssert(this.$section, "input[name='student-select-grade']"); this.$progress_link = findAndAssert(this.$section, 'a.progress-link'); @@ -65,16 +66,51 @@ this.$btn_task_history_all = this.$section.find("input[name='task-history-all']"); this.$table_task_history_all = this.$section.find('.task-history-all-table'); this.instructor_tasks = new (PendingInstructorTasks())(this.$section); - this.$request_err = findAndAssert(this.$section, '.student-specific-container .request-response-error'); + this.$request_err_enrollment_status = findAndAssert(this.$section, '.student-enrollment-status-container .request-response-error'); + this.$request_err_progress = findAndAssert(this.$section, '.student-progress-container .request-response-error'); this.$request_err_grade = findAndAssert(this.$section, '.student-grade-container .request-response-error'); this.$request_err_ee = this.$section.find('.entrance-exam-grade-container .request-response-error'); this.$request_response_error_all = this.$section.find('.course-specific-container .request-response-error'); + this.$enrollment_status_link = findAndAssert(this.$section, 'a.enrollment-status-link'); + this.$enrollment_status = findAndAssert(this.$section, '.student-enrollment-status'); + this.$enrollment_status_link.click(function(e) { + var errorMessage, fullErrorMessage, uniqStudentIdentifier; + e.preventDefault(); + uniqStudentIdentifier = studentadmin.$field_student_select_enrollment_status.val(); + if (!uniqStudentIdentifier) { + studentadmin.$enrollment_status.text(''); + return studentadmin.$request_err_enrollment_status.text( + gettext('Please enter a student email address or username.') + ); + } + errorMessage = gettext("Error getting enrollment status for '<%- student_id %>'. Make sure that the student identifier is spelled correctly."); // eslint-disable-line max-len + fullErrorMessage = _.template(errorMessage)({ + student_id: uniqStudentIdentifier + }); + studentadmin.$enrollment_status.text(gettext("Retrieving enrollment status...")); + return $.ajax({ + type: 'POST', + dataType: 'json', + url: studentadmin.$enrollment_status_link.data('endpoint'), + data: { + course_id: studentadmin.$enrollment_status_link.data('course-id'), + unique_student_identifier: uniqStudentIdentifier + }, + success: studentadmin.clear_errors_then(function(data) { + return studentadmin.$enrollment_status.text(data.enrollment_status); + }), + error: statusAjaxError(function() { + studentadmin.$enrollment_status.text(''); + return studentadmin.$request_err_enrollment_status.text(fullErrorMessage); + }) + }); + }); this.$progress_link.click(function(e) { var errorMessage, fullErrorMessage, uniqStudentIdentifier; e.preventDefault(); uniqStudentIdentifier = studentadmin.$field_student_select_progress.val(); if (!uniqStudentIdentifier) { - return studentadmin.$request_err.text( + return studentadmin.$request_err_progress.text( gettext('Please enter a student email address or username.') ); } @@ -94,7 +130,7 @@ return window.location; }), error: statusAjaxError(function() { - return studentadmin.$request_err.text(fullErrorMessage); + return studentadmin.$request_err_progress.text(fullErrorMessage); }) }); }); @@ -631,7 +667,8 @@ }; StudentAdmin.prototype.clear_errors_then = function(cb) { - this.$request_err.empty(); + this.$request_err_enrollment_status.empty(); + this.$request_err_progress.empty(); this.$request_err_grade.empty(); this.$request_err_ee.empty(); this.$request_response_error_all.empty(); @@ -641,7 +678,8 @@ }; StudentAdmin.prototype.clear_errors = function() { - this.$request_err.empty(); + this.$request_err_enrollment_status.empty(); + this.$request_err_progress.empty(); this.$request_err_grade.empty(); this.$request_err_ee.empty(); return this.$request_response_error_all.empty(); diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html index 0304df0846cc0e52b6018e70b24f57c769879842..ad47b85ef73e87984d3fa011911d050082e31871 100644 --- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html +++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html @@ -14,7 +14,29 @@ %endif </div> -<div class="student-specific-container action-type-container"> +<div class="student-enrollment-status-container action-type-container"> + <h4 class="hd hd-4">${_("View a specific learner's enrollment status")}</h4> + <div class="request-response-error"></div> + <label for="student-select-enrollment-status"> + ${_("Learner's {platform_name} email address or username *").format(platform_name=settings.PLATFORM_NAME)} + </label> + <br> + <input type="text" name="student-select-enrollment-status" placeholder="${_('Learner email address or username')}" > + + <blockquote class="student-enrollment-status"></blockquote> + + <br><br> + <div class="enrollment-status-link-wrapper"> + <span name="enrollment-status-link"> + <a href="" class="enrollment-status-link" data-endpoint="${ section_data['get_student_enrollment_status_url'] }"> + ${_("View Enrollment Status")} + </a> + </span> + </div> + <hr> +</div> + +<div class="student-progress-container action-type-container"> <h4 class="hd hd-4">${_("View a specific learner's grades and progress")}</h4> <div class="request-response-error"></div> <label for="student-select-progress">