From bf5a801fa8979d68a9a4a33647ef6cd6bd84a594 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 27 Sep 2013 11:04:51 -0400 Subject: [PATCH 01/13] Make discussion elements change color on focus Previously, colors changed to afford interaction only on hover; now, all elements with a hover color change behave the same way on focus. --- lms/static/sass/_discussion.scss | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index a685404d34..cd87927fc1 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -12,7 +12,7 @@ text-shadow: 0 1px 0 rgba(0, 0, 0, .3); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset, 0 1px 1px rgba(0, 0, 0, .15); - &:hover { + &:hover, &:focus { border-color: #297095; @include linear-gradient(top, #4fbbe4, #2090d0); } @@ -32,7 +32,7 @@ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset, 0 1px 1px rgba(0, 0, 0, .15); - &:hover { + &:hover, &:focus { @include linear-gradient(top, $white, #ddd); } } @@ -51,7 +51,7 @@ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.6); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset, 0 1px 1px rgba(0, 0, 0, .15); - &:hover { + &:hover, &:focus { background: -webkit-linear-gradient(top, #888, #666); } } @@ -217,8 +217,7 @@ body.discussion { color: #eee; @include transition(none); - &:hover, - &.focused { + &:hover, &:focus { background-color: #666; } @@ -305,7 +304,7 @@ body.discussion { padding-bottom: 2px; height: 37px; - &:hover { + &:hover, &:focus { border-color: #222; } } @@ -436,7 +435,7 @@ body.discussion { height: 37px; border-color: #333; - &:hover { + &:hover, &:focus { border-color: #222; } } @@ -714,7 +713,7 @@ body.discussion { height: 100%; background-color: #dedede; - &:hover { + &:hover, &:focus { background-color: $white; } } @@ -881,8 +880,7 @@ body.discussion { display: none; } - &:hover, - &.focused { + &:hover, &:focus { background-color: #636363; } @@ -1015,7 +1013,7 @@ body.discussion { color: #333; line-height: 17px; - &:hover { + &:hover, &:focus { @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, .2)); color: #333; } @@ -1067,7 +1065,7 @@ body.discussion { line-height: 33px; text-align: center; - &:hover { + &:hover, &:focus { background-image: none; background-color: #e6e6e6; } @@ -1086,7 +1084,7 @@ body.discussion { background-color: $white; @include clearfix; - &:hover { + &:hover, &:focus { @include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0)); background-color: #eee; } @@ -2225,7 +2223,7 @@ body.discussion { padding-bottom: 2px; border-color: #333; - &:hover { + &:hover, &:focus { border-color: #222; } } @@ -2522,7 +2520,7 @@ body.discussion { margin-top: $baseline/2; padding-bottom: 2px; - &:hover { + &:hover, &:focus { border-color: #222; } } @@ -2596,7 +2594,7 @@ body.discussion { cursor: pointer; } - &:hover { + &:hover, &:focus { @include transition(opacity .2s linear 0s); opacity: 1.0; } @@ -2659,7 +2657,7 @@ display:none; cursor:pointer; opacity: 0.8; - &:hover { + &:hover, &:focus { @include transition(opacity .2s linear 0s); opacity: 1.0; } From 8a2bd25b7c6e3906b98eb4ff109e530f4ad7c687 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Tue, 24 Sep 2013 10:57:39 -0400 Subject: [PATCH 02/13] Visual rearrangement of new dash "Student Admin" page --- .../instructor/views/instructor_dashboard.py | 23 +++--- .../instructor_dashboard/student_admin.coffee | 23 +++--- .../courseware/instructor_dashboard.html | 2 +- .../instructor_dashboard_2/course_info.html | 18 +++++ .../instructor_dashboard_2/student_admin.html | 72 +++++++++++++------ 5 files changed, 93 insertions(+), 45 deletions(-) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 031eac266b..e248d47a59 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -38,7 +38,7 @@ def instructor_dashboard_2(request, course_id): raise Http404() sections = [ - _section_course_info(course_id), + _section_course_info(course_id, access), _section_membership(course_id, access), _section_student_admin(course_id, access), _section_data_download(course_id), @@ -67,18 +67,21 @@ section_display_name will be used to generate link titles in the nav bar. """ # pylint: disable=W0105 -def _section_course_info(course_id): +def _section_course_info(course_id, access): """ Provide data for the corresponding dashboard section """ course = get_course_by_id(course_id, depth=None) - section_data = {} - section_data['section_key'] = 'course_info' - section_data['section_display_name'] = _('Course Info') - section_data['course_id'] = course_id - section_data['course_display_name'] = course.display_name - section_data['enrollment_count'] = CourseEnrollment.objects.filter(course_id=course_id).count() - section_data['has_started'] = course.has_started() - section_data['has_ended'] = course.has_ended() + section_data = { + 'section_key': 'course_info', + 'section_display_name': _('Course Info'), + 'course_id': course_id, + 'access': access, + 'course_display_name': course.display_name, + 'enrollment_count': CourseEnrollment.objects.filter(course_id=course_id).count(), + 'has_started': course.has_started(), + 'has_ended': course.has_ended(), + 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}), + } try: advance = lambda memo, (letter, score): "{}: {}, ".format(letter, score) + memo diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee index 7e40eb98d4..e607a463fc 100644 --- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee +++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee @@ -80,12 +80,13 @@ class StudentAdmin # gather buttons # some buttons are optional because they can be flipped by the instructor task feature switch # student-specific - @$field_student_select = find_and_assert @$section, "input[name='student-select']" + @$field_student_select_progress = find_and_assert @$section, "input[name='student-select-progress']" + @$field_student_select_grade = find_and_assert @$section, "input[name='student-select-grade']" @$progress_link = find_and_assert @$section, "a.progress-link" - @$btn_enroll = find_and_assert @$section, "input[name='enroll']" - @$btn_unenroll = find_and_assert @$section, "input[name='unenroll']" @$field_problem_select_single = find_and_assert @$section, "input[name='problem-select-single']" @$btn_reset_attempts_single = find_and_assert @$section, "input[name='reset-attempts-single']" + @$btn_enroll = @$section.find "input[name='enroll']" + @$btn_unenroll = @$section.find "input[name='unenroll']" @$btn_delete_state_single = @$section.find "input[name='delete-state-single']" @$btn_rescore_problem_single = @$section.find "input[name='rescore-problem-single']" @$btn_task_history_single = @$section.find "input[name='task-history-single']" @@ -117,7 +118,7 @@ class StudentAdmin # go to student progress page @$progress_link.click (e) => e.preventDefault() - email = @$field_student_select.val() + email = @$field_student_select_progress.val() $.ajax dataType: 'json' @@ -131,7 +132,7 @@ class StudentAdmin @$btn_enroll.click => send_data = action: 'enroll' - emails: @$field_student_select.val() + emails: @$field_student_select_progress.val() auto_enroll: false $.ajax @@ -145,7 +146,7 @@ class StudentAdmin @$btn_unenroll.click => send_data = action: 'unenroll' - emails: @$field_student_select.val() + emails: @$field_student_select_progress.val() $.ajax dataType: 'json' @@ -157,7 +158,7 @@ class StudentAdmin # reset attempts for student on problem @$btn_reset_attempts_single.click => send_data = - student_email: @$field_student_select.val() + student_email: @$field_student_select_grade.val() problem_to_reset: @$field_problem_select_single.val() delete_module: false @@ -170,10 +171,10 @@ class StudentAdmin # delete state for student on problem @$btn_delete_state_single.click => confirm_then - msg: "Delete student '#{@$field_student_select.val()}'s state on problem '#{@$field_problem_select_single.val()}'?" + msg: "Delete student '#{@$field_student_select_grade.val()}'s state on problem '#{@$field_problem_select_single.val()}'?" ok: => send_data = - student_email: @$field_student_select.val() + student_email: @$field_student_select_grade.val() problem_to_reset: @$field_problem_select_single.val() delete_module: true @@ -187,7 +188,7 @@ class StudentAdmin # start task to rescore problem for student @$btn_rescore_problem_single.click => send_data = - student_email: @$field_student_select.val() + student_email: @$field_student_select_grade.val() problem_to_reset: @$field_problem_select_single.val() $.ajax @@ -200,7 +201,7 @@ class StudentAdmin # list task history for student+problem @$btn_task_history_single.click => send_data = - student_email: @$field_student_select.val() + student_email: @$field_student_select_grade.val() problem_urlname: @$field_problem_select_single.val() if not send_data.student_email diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 7f41c82c9d..7b06d8e309 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -240,7 +240,7 @@ function goto( mode)
%endif -

${_("Student-specific grade inspection and adjustment")}

+

${_("Student-specific grade inspection and adjustment")}

${_("Specify the {platform_name} email address or username of a student here:").format(platform_name=settings.PLATFORM_NAME)} diff --git a/lms/templates/instructor/instructor_dashboard_2/course_info.html b/lms/templates/instructor/instructor_dashboard_2/course_info.html index 9b4114d95c..cb113e1846 100644 --- a/lms/templates/instructor/instructor_dashboard_2/course_info.html +++ b/lms/templates/instructor/instructor_dashboard_2/course_info.html @@ -38,8 +38,21 @@ ## ${ section_data['offline_grades'] } ## +%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']: +

+
+

${_("Pending Instructor Tasks")}

+

${_("The status for any active tasks appears in a table below.")}

+ +
+
+ +%endif + %if len(section_data['course_errors']):
+
+

${_("Course Warnings")}:

@@ -52,5 +65,10 @@
%endfor
+

+
%endif + + + diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html index 6b6da617f6..001987ec18 100644 --- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html +++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html @@ -2,25 +2,47 @@ <%page args="section_data"/>

-

${_("Student-specific grade adjustment")}

+

${_("Student-specific grade inspection")}

- - +

+ + ${_("Specify the {platform_name} email address or username of a student here:").format(platform_name=settings.PLATFORM_NAME)} + +


+

+ ${_("Click this link to view the student's progress page:")} + ${_("Student Progress Page")} +

+

+ + -

${_('Specify a particular problem in the course here by its url:')}

+
+ +

${_("Student-specific grade adjustment")}

+ +

+ + ${_("Specify the {platform_name} email address or username of a student here:").format(platform_name=settings.PLATFORM_NAME)} + +

+
+ +

${_('Specify a particular problem in the course here by its url:')} +

+

${_('You may use just the "urlname" if a problem, or "modulename/urlname" if not. (For example, if the location is {location1}, then just provide the {urlname1}. If the location is {location2}, then provide {urlname2}.)').format( location1="i4x://university/course/problem/problemname", @@ -29,20 +51,31 @@ urlname2="notaproblem/someothername") }

- - %if section_data['access']['instructor']: -

${_('You may also delete the entire state of a student for the specified module:')}

- - %endif +

+ ${_("Next, select an action to perform for the given user and problem:")} +

+ +

+ + %if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']: %endif +

+ +

+ %if section_data['access']['instructor']: +

${_('You may also delete the entire state of a student for the specified problem:')}

+ + %endif +

+ %if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:

- ${_("Rescoring runs in the background, and status for active tasks will appear in a table below. " + ${_("Rescoring runs in the background, and status for active tasks will appear in a table on the Course Info tab. " "To see status for all tasks submitted for this problem and student, click on this button:")}

@@ -76,18 +109,11 @@

- ${_("These actions run in the background, and status for active tasks will appear in a table below. " + ${_("These actions run in the background, and status for active tasks will appear in a table on the Course Info tab. " "To see status for all tasks submitted for this problem, click on this button")}:

- - -
-
-

${_("Pending Instructor Tasks")}

-
-
%endif From 07e76b3b2f92d7f97e2ad1f3a3f094885426b55e Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Thu, 26 Sep 2013 08:09:09 -0400 Subject: [PATCH 03/13] Enable use of student usernames on student admin page This is in addition to email addresses, which also work. --- lms/djangoapps/instructor/views/api.py | 53 ++++++++++--------- lms/djangoapps/instructor/views/tools.py | 17 ++++++ .../instructor_dashboard/student_admin.coffee | 22 ++++---- 3 files changed, 55 insertions(+), 37 deletions(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 8a552feb66..9e58ecea5f 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -33,7 +33,7 @@ import instructor_task.api from instructor_task.api_helper import AlreadyRunningError import instructor.enrollment as enrollment from instructor.enrollment import enroll_email, unenroll_email -from instructor.views.tools import strip_if_string +from instructor.views.tools import strip_if_string, get_student_from_identifier import instructor.access as access import analytics.basic import analytics.distributions @@ -456,20 +456,19 @@ def get_distribution(request, course_id): @common_exceptions_400 @require_level('staff') @require_query_params( - student_email="email of student for whom to get progress url" + unique_student_identifier="email or username of student for whom to get progress url" ) def get_student_progress_url(request, course_id): """ Get the progress url of a student. Limited to staff access. - Takes query paremeter student_email and if the student exists + Takes query paremeter unique_student_identifier and if the student exists returns e.g. { 'progress_url': '/../...' } """ - student_email = strip_if_string(request.GET.get('student_email')) - user = User.objects.get(email=student_email) + user = get_student_from_identifier(request.GET.get('unique_student_identifier')) progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': user.id}) @@ -496,7 +495,7 @@ def reset_student_attempts(request, course_id): Takes some of the following query paremeters - problem_to_reset is a urlname of a problem - - student_email is an email + - unique_student_identifier is an email or username - all_students is a boolean requires instructor access mutually exclusive with delete_module @@ -510,14 +509,14 @@ def reset_student_attempts(request, course_id): ) problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) - student_email = strip_if_string(request.GET.get('student_email')) + student = get_student_from_identifier(request.GET.get('unique_student_identifier')) all_students = request.GET.get('all_students', False) in ['true', 'True', True] delete_module = request.GET.get('delete_module', False) in ['true', 'True', True] # parameter combinations - if all_students and student_email: + if all_students and student: return HttpResponseBadRequest( - "all_students and student_email are mutually exclusive." + "all_students and unique_student_identifier are mutually exclusive." ) if all_students and delete_module: return HttpResponseBadRequest( @@ -534,9 +533,8 @@ def reset_student_attempts(request, course_id): response_payload = {} response_payload['problem_to_reset'] = problem_to_reset - if student_email: + if student: try: - student = User.objects.get(email=student_email) enrollment.reset_student_attempts(course_id, student, module_state_key, delete_module=delete_module) except StudentModule.DoesNotExist: return HttpResponseBadRequest("Module does not exist.") @@ -561,21 +559,24 @@ def rescore_problem(request, course_id): Takes either of the following query paremeters - problem_to_reset is a urlname of a problem - - student_email is an email + - unique_student_identifier is an email or username - all_students is a boolean - all_students and student_email cannot both be present. + all_students and unique_student_identifier cannot both be present. """ problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) - student_email = strip_if_string(request.GET.get('student_email', False)) + student = request.GET.get('unique_student_identifier', None) + if student is not None: + student = get_student_from_identifier(student) + all_students = request.GET.get('all_students') in ['true', 'True', True] - if not (problem_to_reset and (all_students or student_email)): + if not (problem_to_reset and (all_students or student)): return HttpResponseBadRequest("Missing query parameters.") - if all_students and student_email: + if all_students and student: return HttpResponseBadRequest( - "Cannot rescore with all_students and student_email." + "Cannot rescore with all_students and unique_student_identifier." ) module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset) @@ -583,9 +584,8 @@ def rescore_problem(request, course_id): response_payload = {} response_payload['problem_to_reset'] = problem_to_reset - if student_email: - response_payload['student_email'] = student_email - student = User.objects.get(email=student_email) + if student: + response_payload['student'] = student instructor_task.api.submit_rescore_problem_for_student(request, course_id, module_state_key, student) response_payload['task'] = 'created' elif all_students: @@ -608,21 +608,22 @@ def list_instructor_tasks(request, course_id): Takes optional query paremeters. - With no arguments, lists running tasks. - `problem_urlname` lists task history for problem - - `problem_urlname` and `student_email` lists task + - `problem_urlname` and `unique_student_identifier` lists task history for problem AND student (intersection) """ problem_urlname = strip_if_string(request.GET.get('problem_urlname', False)) - student_email = strip_if_string(request.GET.get('student_email', False)) + student = request.GET.get('unique_student_identifier', None) + if student is not None: + student = get_student_from_identifier(student) - if student_email and not problem_urlname: + if student and not problem_urlname: return HttpResponseBadRequest( - "student_email must accompany problem_urlname" + "unique_student_identifier must accompany problem_urlname" ) if problem_urlname: module_state_key = _msk_from_problem_urlname(course_id, problem_urlname) - if student_email: - student = User.objects.get(email=student_email) + if student: tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key, student) else: tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key) diff --git a/lms/djangoapps/instructor/views/tools.py b/lms/djangoapps/instructor/views/tools.py index 11fc135976..eab0e66196 100644 --- a/lms/djangoapps/instructor/views/tools.py +++ b/lms/djangoapps/instructor/views/tools.py @@ -1,7 +1,24 @@ """ Tools for the instructor dashboard """ +from django.contrib.auth.models import User + def strip_if_string(value): if isinstance(value, basestring): return value.strip() return value + +def get_student_from_identifier(unique_student_identifier): + """ + Gets a student object using either an email address or username. + + Returns the student object associated with `unique_student_identifier` + + Raises User.DoesNotExist if no user object can be found. + """ + unique_student_identifier = strip_if_string(unique_student_identifier) + if "@" in unique_student_identifier: + student = User.objects.get(email=unique_student_identifier) + else: + student = User.objects.get(username=unique_student_identifier) + return student diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee index e607a463fc..10d83c4a00 100644 --- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee +++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee @@ -118,15 +118,15 @@ class StudentAdmin # go to student progress page @$progress_link.click (e) => e.preventDefault() - email = @$field_student_select_progress.val() + unique_student_identifier = @$field_student_select_progress.val() $.ajax dataType: 'json' url: @$progress_link.data 'endpoint' - data: student_email: email + data: unique_student_identifier: unique_student_identifier success: @clear_errors_then (data) -> window.location = data.progress_url - error: std_ajax_err => @$request_response_error_single.text "Error getting student progress url for '#{email}'." + error: std_ajax_err => @$request_response_error_single.text "Error getting student progress url for '#{unique_student_identifier}'." # enroll student @$btn_enroll.click => @@ -158,7 +158,7 @@ class StudentAdmin # reset attempts for student on problem @$btn_reset_attempts_single.click => send_data = - student_email: @$field_student_select_grade.val() + unique_student_identifier: @$field_student_select_grade.val() problem_to_reset: @$field_problem_select_single.val() delete_module: false @@ -167,14 +167,14 @@ class StudentAdmin url: @$btn_reset_attempts_single.data 'endpoint' data: send_data success: @clear_errors_then -> console.log 'problem attempts reset' - error: std_ajax_err => @$request_response_error_single.text "Error resetting problem attempts." + error: std_ajax_err => @$request_response_error_single.text "Error resetting problem attempts for problem '#{problem_to_reset}' and student '#{unique_student_identifier}'." # delete state for student on problem @$btn_delete_state_single.click => confirm_then msg: "Delete student '#{@$field_student_select_grade.val()}'s state on problem '#{@$field_problem_select_single.val()}'?" ok: => send_data = - student_email: @$field_student_select_grade.val() + unique_student_identifier: @$field_student_select_grade.val() problem_to_reset: @$field_problem_select_single.val() delete_module: true @@ -188,7 +188,7 @@ class StudentAdmin # start task to rescore problem for student @$btn_rescore_problem_single.click => send_data = - student_email: @$field_student_select_grade.val() + unique_student_identifier: @$field_student_select_grade.val() problem_to_reset: @$field_problem_select_single.val() $.ajax @@ -201,13 +201,13 @@ class StudentAdmin # list task history for student+problem @$btn_task_history_single.click => send_data = - student_email: @$field_student_select_grade.val() + unique_student_identifier: @$field_student_select_grade.val() problem_urlname: @$field_problem_select_single.val() - if not send_data.student_email - return @$request_response_error_single.text "Enter a student email." + if not send_data.unique_student_identifier + return @$request_response_error_single.text "Please enter a student email address or username." if not send_data.problem_urlname - return @$request_response_error_single.text "Enter a problem urlname." + return @$request_response_error_single.text "Please enter a problem urlname." $.ajax dataType: 'json' From 086f544a1a39d3cac22c30bd8c0ee3d693fd0748 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Thu, 26 Sep 2013 17:57:55 -0400 Subject: [PATCH 04/13] Fix, and add more, tests --- lms/djangoapps/instructor/tests/test_api.py | 41 +++++++++++++++++---- lms/djangoapps/instructor/views/api.py | 16 +++++--- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index c5b1b21b52..a32217ab30 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -512,7 +512,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa def test_get_student_progress_url(self): """ Test that progress_url is in the successful response. """ url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id}) - url += "?student_email={}".format( + url += "?unique_student_identifier={}".format( quote(self.students[0].email.encode("utf-8")) ) print url @@ -522,6 +522,19 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa res_json = json.loads(response.content) self.assertIn('progress_url', res_json) + def test_get_student_progress_url_from_uname(self): + """ Test that progress_url is in the successful response. """ + url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id}) + url += "?unique_student_identifier={}".format( + quote(self.students[0].username.encode("utf-8")) + ) + print url + response = self.client.get(url) + print response + self.assertEqual(response.status_code, 200) + res_json = json.loads(response.content) + self.assertIn('progress_url', res_json) + def test_get_student_progress_url_noparams(self): """ Test that the endpoint 404's without the required query params. """ url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id}) @@ -579,7 +592,7 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase) url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, - 'student_email': self.student.email, + 'unique_student_identifier': self.student.email, }) print response.content self.assertEqual(response.status_code, 200) @@ -608,7 +621,7 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase) url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) response = self.client.get(url, { 'problem_to_reset': 'robot-not-a-real-module', - 'student_email': self.student.email, + 'unique_student_identifier': self.student.email, }) print response.content self.assertEqual(response.status_code, 400) @@ -618,7 +631,7 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase) url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, - 'student_email': self.student.email, + 'unique_student_identifier': self.student.email, 'delete_module': True, }) print response.content @@ -634,11 +647,11 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase) ) def test_reset_student_attempts_nonsense(self): - """ Test failure with both student_email and all_students. """ + """ Test failure with both unique_student_identifier and all_students. """ url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, - 'student_email': self.student.email, + 'unique_student_identifier': self.student.email, 'all_students': True, }) print response.content @@ -650,7 +663,19 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase) url = reverse('rescore_problem', kwargs={'course_id': self.course.id}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, - 'student_email': self.student.email, + 'unique_student_identifier': self.student.email, + }) + print response.content + self.assertEqual(response.status_code, 200) + self.assertTrue(act.called) + + @patch.object(instructor_task.api, 'submit_rescore_problem_for_student') + def test_rescore_problem_single_from_uname(self, act): + """ Test rescoring of a single student. """ + url = reverse('rescore_problem', kwargs={'course_id': self.course.id}) + response = self.client.get(url, { + 'problem_to_reset': self.problem_urlname, + 'unique_student_identifier': self.student.username, }) print response.content self.assertEqual(response.status_code, 200) @@ -747,7 +772,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id}) response = self.client.get(url, { 'problem_urlname': self.problem_urlname, - 'student_email': self.student.email, + 'unique_student_identifier': self.student.email, }) print response.content self.assertEqual(response.status_code, 200) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 9e58ecea5f..e0b047604e 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -509,7 +509,10 @@ def reset_student_attempts(request, course_id): ) problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) - student = get_student_from_identifier(request.GET.get('unique_student_identifier')) + student_identifier = request.GET.get('unique_student_identifier', None) + student = None + if student_identifier is not None: + student = get_student_from_identifier(student_identifier) all_students = request.GET.get('all_students', False) in ['true', 'True', True] delete_module = request.GET.get('delete_module', False) in ['true', 'True', True] @@ -538,9 +541,11 @@ def reset_student_attempts(request, course_id): enrollment.reset_student_attempts(course_id, student, module_state_key, delete_module=delete_module) except StudentModule.DoesNotExist: return HttpResponseBadRequest("Module does not exist.") + response_payload['student'] = student_identifier elif all_students: instructor_task.api.submit_reset_problem_attempts_for_all_students(request, course_id, module_state_key) response_payload['task'] = 'created' + response_payload['student'] = 'All Students' else: return HttpResponseBadRequest() @@ -565,9 +570,10 @@ def rescore_problem(request, course_id): all_students and unique_student_identifier cannot both be present. """ problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) - student = request.GET.get('unique_student_identifier', None) - if student is not None: - student = get_student_from_identifier(student) + student_identifier = request.GET.get('unique_student_identifier', None) + student = None + if student_identifier is not None: + student = get_student_from_identifier(student_identifier) all_students = request.GET.get('all_students') in ['true', 'True', True] @@ -585,7 +591,7 @@ def rescore_problem(request, course_id): response_payload['problem_to_reset'] = problem_to_reset if student: - response_payload['student'] = student + response_payload['student'] = student_identifier instructor_task.api.submit_rescore_problem_for_student(request, course_id, module_state_key, student) response_payload['task'] = 'created' elif all_students: From 91d85f5c2a1b1a43f282c64a7f78a84bd2aecc09 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Thu, 26 Sep 2013 18:54:15 -0400 Subject: [PATCH 05/13] Enable alerts when a task succeeds. Note: Alerts are pop-up boxes. Not sure if this is desireable, but I couldn't figure out how to make a success message show up in the same place that error messages do. --- lms/djangoapps/instructor/views/tools.py | 1 + .../instructor_dashboard/student_admin.coffee | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lms/djangoapps/instructor/views/tools.py b/lms/djangoapps/instructor/views/tools.py index eab0e66196..cbf6b6468a 100644 --- a/lms/djangoapps/instructor/views/tools.py +++ b/lms/djangoapps/instructor/views/tools.py @@ -8,6 +8,7 @@ def strip_if_string(value): return value.strip() return value + def get_student_from_identifier(unique_student_identifier): """ Gets a student object using either an email address or username. diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee index 10d83c4a00..3030f622ce 100644 --- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee +++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee @@ -157,16 +157,18 @@ class StudentAdmin # reset attempts for student on problem @$btn_reset_attempts_single.click => + unique_student_identifier = @$field_student_select_grade.val() + problem_to_reset = @$field_problem_select_single.val() send_data = - unique_student_identifier: @$field_student_select_grade.val() - problem_to_reset: @$field_problem_select_single.val() + unique_student_identifier: unique_student_identifier + problem_to_reset: problem_to_reset delete_module: false $.ajax dataType: 'json' url: @$btn_reset_attempts_single.data 'endpoint' data: send_data - success: @clear_errors_then -> console.log 'problem attempts reset' + success: @clear_errors_then -> alert "Success! Problem attempts reset for problem '#{problem_to_reset}' and student '#{unique_student_identifier}'." error: std_ajax_err => @$request_response_error_single.text "Error resetting problem attempts for problem '#{problem_to_reset}' and student '#{unique_student_identifier}'." # delete state for student on problem @@ -182,21 +184,23 @@ class StudentAdmin dataType: 'json' url: @$btn_delete_state_single.data 'endpoint' data: send_data - success: @clear_errors_then -> console.log 'module state deleted' + success: @clear_errors_then -> alert 'Module state successfully deleted.' error: std_ajax_err => @$request_response_error_single.text "Error deleting problem state." # start task to rescore problem for student @$btn_rescore_problem_single.click => + unique_student_identifier = @$field_student_select_grade.val() + problem_to_reset = @$field_problem_select_single.val() send_data = - unique_student_identifier: @$field_student_select_grade.val() - problem_to_reset: @$field_problem_select_single.val() + unique_student_identifier: unique_student_identifier + problem_to_reset: problem_to_reset $.ajax dataType: 'json' url: @$btn_rescore_problem_single.data 'endpoint' data: send_data - success: @clear_errors_then -> console.log 'started rescore problem task' - error: std_ajax_err => @$request_response_error_single.text "Error starting a task to rescore student's problem." + success: @clear_errors_then -> alert "Started rescore problem task for problem '#{problem_to_reset}' and student '#{unique_student_identifier}'. Click the 'Show Background Task History for Student' button to see the status of the task." + error: std_ajax_err => @$request_response_error_single.text "Error starting a task to rescore problem '#{problem_to_reset}' for student '#{unique_student_identifier}'." # list task history for student+problem @$btn_task_history_single.click => @@ -221,30 +225,32 @@ class StudentAdmin @$btn_reset_attempts_all.click => confirm_then msg: "Reset attempts for all students on problem '#{@$field_problem_select_all.val()}'?" ok: => + problem_to_reset = @$field_problem_select_all.val() send_data = all_students: true - problem_to_reset: @$field_problem_select_all.val() + problem_to_reset: problem_to_reset $.ajax dataType: 'json' url: @$btn_reset_attempts_all.data 'endpoint' data: send_data - success: @clear_errors_then -> console.log 'started reset attempts task' + success: @clear_errors_then -> alert "Successfully started task to reset attempts for problem '#{problem_to_reset}'. Click the 'Show Background Task History for Problem' button to see the status of the task." error: std_ajax_err => @$request_response_error_all.text "Error starting a task to reset attempts for all students on this problem." # start task to rescore problem for all students @$btn_rescore_problem_all.click => confirm_then msg: "Rescore problem '#{@$field_problem_select_all.val()}' for all students?" ok: => + problem_to_reset = @$field_problem_select_all.val() send_data = all_students: true - problem_to_reset: @$field_problem_select_all.val() + problem_to_reset: problem_to_reset $.ajax dataType: 'json' url: @$btn_rescore_problem_all.data 'endpoint' data: send_data - success: @clear_errors_then -> console.log 'started rescore problem task' + success: @clear_errors_then -> alert "Successfully started task to rescore problem '#{problem_to_reset}' for all students. Click the 'Show Background Task History for Problem' button to see the status of the task." error: std_ajax_err => @$request_response_error_all.text "Error starting a task to rescore this problem for all students." # list task history for problem From 3813e51cfbb684c1a7b5efd93378eeec5a5a71e8 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Fri, 27 Sep 2013 09:42:51 -0400 Subject: [PATCH 06/13] Add more feedback for instructors on Student Admin section of the dash. --- .../instructor_dashboard/student_admin.coffee | 93 +++++++++++++------ .../instructor_dashboard_2/student_admin.html | 4 +- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee index 3030f622ce..3e5c8c27c2 100644 --- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee +++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee @@ -101,7 +101,8 @@ class StudentAdmin @$table_running_tasks = @$section.find ".running-tasks-table" # response areas - @$request_response_error_single = find_and_assert @$section, ".student-specific-container .request-response-error" + @$request_response_error_progress = find_and_assert @$section, ".student-specific-container .request-response-error" + @$request_response_error_grade = find_and_assert @$section, ".student-grade-container .request-response-error" @$request_response_error_all = @$section.find ".course-specific-container .request-response-error" # start polling for task list @@ -119,6 +120,8 @@ class StudentAdmin @$progress_link.click (e) => e.preventDefault() unique_student_identifier = @$field_student_select_progress.val() + if not unique_student_identifier + return @$request_response_error_progress.text "Please enter a student email address or username." $.ajax dataType: 'json' @@ -126,7 +129,7 @@ class StudentAdmin data: unique_student_identifier: unique_student_identifier success: @clear_errors_then (data) -> window.location = data.progress_url - error: std_ajax_err => @$request_response_error_single.text "Error getting student progress url for '#{unique_student_identifier}'." + error: std_ajax_err => @$request_response_error_progress.text "Error getting student progress url for '#{unique_student_identifier}'." # enroll student @$btn_enroll.click => @@ -140,7 +143,7 @@ class StudentAdmin url: @$btn_enroll.data 'endpoint' data: send_data success: @clear_errors_then -> console.log "student #{send_data.emails} enrolled" - error: std_ajax_err => @$request_response_error_single.text "Error enrolling student '#{send_data.emails}'." + error: std_ajax_err => @$request_response_error_progress.text "Error enrolling student '#{send_data.emails}'." # unenroll student @$btn_unenroll.click => @@ -153,12 +156,16 @@ class StudentAdmin url: @$btn_unenroll.data 'endpoint' data: send_data success: @clear_errors_then -> console.log "student #{send_data.emails} unenrolled" - error: std_ajax_err => @$request_response_error_single.text "Error unenrolling student '#{send_data.emails}'." + error: std_ajax_err => @$request_response_error_progress.text "Error unenrolling student '#{send_data.emails}'." # reset attempts for student on problem @$btn_reset_attempts_single.click => unique_student_identifier = @$field_student_select_grade.val() problem_to_reset = @$field_problem_select_single.val() + if not unique_student_identifier + return @$request_response_error_grade.text "Please enter a student email address or username." + if not problem_to_reset + return @$request_response_error_grade.text "Please enter a problem urlname." send_data = unique_student_identifier: unique_student_identifier problem_to_reset: problem_to_reset @@ -169,15 +176,21 @@ class StudentAdmin url: @$btn_reset_attempts_single.data 'endpoint' data: send_data success: @clear_errors_then -> alert "Success! Problem attempts reset for problem '#{problem_to_reset}' and student '#{unique_student_identifier}'." - error: std_ajax_err => @$request_response_error_single.text "Error resetting problem attempts for problem '#{problem_to_reset}' and student '#{unique_student_identifier}'." + error: std_ajax_err => @$request_response_error_grade.text "Error resetting problem attempts for problem '#{problem_to_reset}' and student '#{unique_student_identifier}'." # delete state for student on problem - @$btn_delete_state_single.click => confirm_then - msg: "Delete student '#{@$field_student_select_grade.val()}'s state on problem '#{@$field_problem_select_single.val()}'?" - ok: => + @$btn_delete_state_single.click => + unique_student_identifier = @$field_student_select_grade.val() + problem_to_reset = @$field_problem_select_single.val() + if not unique_student_identifier + return @$request_response_error_grade.text "Please enter a student email address or username." + if not problem_to_reset + return @$request_response_error_grade.text "Please enter a problem urlname." + + if window.confirm "Delete student '#{unique_student_identifier}'s state on problem '#{problem_to_reset}'?" send_data = - unique_student_identifier: @$field_student_select_grade.val() - problem_to_reset: @$field_problem_select_single.val() + unique_student_identifier: unique_student_identifier + problem_to_reset: problem_to_reset delete_module: true $.ajax @@ -185,12 +198,18 @@ class StudentAdmin url: @$btn_delete_state_single.data 'endpoint' data: send_data success: @clear_errors_then -> alert 'Module state successfully deleted.' - error: std_ajax_err => @$request_response_error_single.text "Error deleting problem state." + error: std_ajax_err => @$request_response_error_grade.text "Error deleting problem state." + else + @clear_errors() # start task to rescore problem for student @$btn_rescore_problem_single.click => unique_student_identifier = @$field_student_select_grade.val() problem_to_reset = @$field_problem_select_single.val() + if not unique_student_identifier + return @$request_response_error_grade.text "Please enter a student email address or username." + if not problem_to_reset + return @$request_response_error_grade.text "Please enter a problem urlname." send_data = unique_student_identifier: unique_student_identifier problem_to_reset: problem_to_reset @@ -200,18 +219,19 @@ class StudentAdmin url: @$btn_rescore_problem_single.data 'endpoint' data: send_data success: @clear_errors_then -> alert "Started rescore problem task for problem '#{problem_to_reset}' and student '#{unique_student_identifier}'. Click the 'Show Background Task History for Student' button to see the status of the task." - error: std_ajax_err => @$request_response_error_single.text "Error starting a task to rescore problem '#{problem_to_reset}' for student '#{unique_student_identifier}'." + error: std_ajax_err => @$request_response_error_grade.text "Error starting a task to rescore problem '#{problem_to_reset}' for student '#{unique_student_identifier}'." # list task history for student+problem @$btn_task_history_single.click => + unique_student_identifier = @$field_student_select_grade.val() + problem_to_reset = @$field_problem_select_single.val() + if not unique_student_identifier + return @$request_response_error_grade.text "Please enter a student email address or username." + if not problem_to_reset + return @$request_response_error_grade.text "Please enter a problem urlname." send_data = - unique_student_identifier: @$field_student_select_grade.val() - problem_urlname: @$field_problem_select_single.val() - - if not send_data.unique_student_identifier - return @$request_response_error_single.text "Please enter a student email address or username." - if not send_data.problem_urlname - return @$request_response_error_single.text "Please enter a problem urlname." + unique_student_identifier: unique_student_identifier + problem_urlname: problem_to_reset $.ajax dataType: 'json' @@ -219,13 +239,14 @@ class StudentAdmin data: send_data success: @clear_errors_then (data) => create_task_list_table @$table_task_history_single, data.tasks - error: std_ajax_err => @$request_response_error_single.text "Error getting task history for student+problem" + error: std_ajax_err => @$request_response_error_grade.text "Error getting task history for student '#{unique_student_identifier}' and problem '#{problem_to_reset}'." # start task to reset attempts on problem for all students - @$btn_reset_attempts_all.click => confirm_then - msg: "Reset attempts for all students on problem '#{@$field_problem_select_all.val()}'?" - ok: => - problem_to_reset = @$field_problem_select_all.val() + @$btn_reset_attempts_all.click => + problem_to_reset = @$field_problem_select_all.val() + if not problem_to_reset + return @$request_response_error_all.text "Please enter a problem urlname." + if window.confirm "Reset attempts for all students on problem '#{@$field_problem_select_all.val()}'?" send_data = all_students: true problem_to_reset: problem_to_reset @@ -236,12 +257,15 @@ class StudentAdmin data: send_data success: @clear_errors_then -> alert "Successfully started task to reset attempts for problem '#{problem_to_reset}'. Click the 'Show Background Task History for Problem' button to see the status of the task." error: std_ajax_err => @$request_response_error_all.text "Error starting a task to reset attempts for all students on this problem." + else + @clear_errors() # start task to rescore problem for all students - @$btn_rescore_problem_all.click => confirm_then - msg: "Rescore problem '#{@$field_problem_select_all.val()}' for all students?" - ok: => - problem_to_reset = @$field_problem_select_all.val() + @$btn_rescore_problem_all.click => + problem_to_reset = @$field_problem_select_all.val() + if not problem_to_reset + return @$request_response_error_all.text "Please enter a problem urlname." + if window.confirm "Rescore problem '#{@$field_problem_select_all.val()}' for all students?" send_data = all_students: true problem_to_reset: problem_to_reset @@ -252,6 +276,8 @@ class StudentAdmin data: send_data success: @clear_errors_then -> alert "Successfully started task to rescore problem '#{problem_to_reset}' for all students. Click the 'Show Background Task History for Problem' button to see the status of the task." error: std_ajax_err => @$request_response_error_all.text "Error starting a task to rescore this problem for all students." + else + @clear_errors() # list task history for problem @$btn_task_history_all.click => @@ -259,7 +285,7 @@ class StudentAdmin problem_urlname: @$field_problem_select_all.val() if not send_data.problem_urlname - return @$request_response_error_all.text "Enter a problem urlname." + return @$request_response_error_all.text "Please enter a problem urlname." $.ajax dataType: 'json' @@ -279,11 +305,18 @@ class StudentAdmin # wraps a function, but first clear the error displays clear_errors_then: (cb) -> - @$request_response_error_single.empty() + @$request_response_error_progress.empty() + @$request_response_error_grade.empty() @$request_response_error_all.empty() -> cb?.apply this, arguments + + clear_errors: -> + @$request_response_error_progress.empty() + @$request_response_error_grade.empty() + @$request_response_error_all.empty() + # handler for when the section title is clicked. onClickTitle: -> @task_poller?.start() diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html index 001987ec18..f3fc7b6054 100644 --- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html +++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html @@ -29,9 +29,11 @@ -->
+ +

${_("Student-specific grade adjustment")}

- +

${_("Specify the {platform_name} email address or username of a student here:").format(platform_name=settings.PLATFORM_NAME)} From f4971e753b729bca6eb1e86fbdae374a65b3b6e9 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 27 Sep 2013 13:45:38 -0400 Subject: [PATCH 07/13] Add accessible labels for forum post body inputs --- lms/static/coffee/src/customwmd.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lms/static/coffee/src/customwmd.coffee b/lms/static/coffee/src/customwmd.coffee index 838112059e..2dabc3ed15 100644 --- a/lms/static/coffee/src/customwmd.coffee +++ b/lms/static/coffee/src/customwmd.coffee @@ -131,9 +131,11 @@ $ -> initialText = $elem.html() $elem.empty() _append = appended_id || "" + wmdInputId = "wmd-input#{_append}" $wmdPanel = $("

").addClass("wmd-panel") .append($("
").attr("id", "wmd-button-bar#{_append}")) - .append($("