From 297206f260a368f0cfdb0a7c3c5908593f60385e Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Thu, 16 May 2013 19:03:50 -0400 Subject: [PATCH] Add background task history to instructor dash (as table). Add task-history-per-student button, and fix display of task messages. --- lms/djangoapps/courseware/task_queue.py | 45 ++-- .../courseware/tests/test_task_queue.py | 2 +- lms/djangoapps/instructor/views.py | 239 +++++++++--------- 3 files changed, 139 insertions(+), 147 deletions(-) diff --git a/lms/djangoapps/courseware/task_queue.py b/lms/djangoapps/courseware/task_queue.py index 90cdd7f765..85649c29f2 100644 --- a/lms/djangoapps/courseware/task_queue.py +++ b/lms/djangoapps/courseware/task_queue.py @@ -143,6 +143,8 @@ def _update_course_task_log(course_task_log_entry, task_result): Calculates json to store in task_progress field. """ + # Just pull values out of the result object once. If we check them later, + # the state and result may have changed. task_id = task_result.task_id result_state = task_result.state returned_result = task_result.result @@ -240,39 +242,36 @@ def _get_course_task_log_status(task_id): # define ajax return value: output = {} - # if the task is already known to be done, then there's no reason to query + # if the task is not already known to be done, then we need to query # the underlying task's result object: if course_task_log_entry.task_state not in READY_STATES: - # we need to get information from the task result directly now. - - # Just create the result object, and pull values out once. - # (If we check them later, the state and result may have changed.) result = AsyncResult(task_id) output.update(_update_course_task_log(course_task_log_entry, result)) elif course_task_log_entry.task_progress is not None: # task is already known to have finished, but report on its status: output['task_progress'] = json.loads(course_task_log_entry.task_progress) - if course_task_log_entry.task_state == 'FAILURE': - output['message'] = output['task_progress']['message'] # output basic information matching what's stored in CourseTaskLog: output['task_id'] = course_task_log_entry.task_id output['task_state'] = course_task_log_entry.task_state output['in_progress'] = course_task_log_entry.task_state not in READY_STATES - if course_task_log_entry.task_state == 'SUCCESS': - succeeded, message = _get_task_completion_message(course_task_log_entry) + if course_task_log_entry.task_state in READY_STATES: + succeeded, message = get_task_completion_message(course_task_log_entry) output['message'] = message output['succeeded'] = succeeded return output -def _get_task_completion_message(course_task_log_entry): +def get_task_completion_message(course_task_log_entry): """ Construct progress message from progress information in CourseTaskLog entry. Returns (boolean, message string) duple. + + Used for providing messages to course_task_log_status(), as well as + external calls for providing course task submission history information. """ succeeded = False @@ -281,30 +280,36 @@ def _get_task_completion_message(course_task_log_entry): return (succeeded, "No status information available") task_progress = json.loads(course_task_log_entry.task_progress) + if course_task_log_entry.task_state in ['FAILURE', 'REVOKED']: + return(succeeded, task_progress['message']) + action_name = task_progress['action_name'] num_attempted = task_progress['attempted'] num_updated = task_progress['updated'] - # num_total = task_progress['total'] + num_total = task_progress['total'] if course_task_log_entry.student is not None: if num_attempted == 0: - msg = "Unable to find submission to be {action} for student '{student}' and problem '{problem}'." + msg = "Unable to find submission to be {action} for student '{student}'" elif num_updated == 0: - msg = "Problem failed to be {action} for student '{student}' and problem '{problem}'" + msg = "Problem failed to be {action} for student '{student}'" else: succeeded = True - msg = "Problem successfully {action} for student '{student}' and problem '{problem}'" + msg = "Problem successfully {action} for student '{student}'" elif num_attempted == 0: - msg = "Unable to find any students with submissions to be {action} for problem '{problem}'." + msg = "Unable to find any students with submissions to be {action}" elif num_updated == 0: - msg = "Problem failed to be {action} for any of {attempted} students for problem '{problem}'" + msg = "Problem failed to be {action} for any of {attempted} students" elif num_updated == num_attempted: succeeded = True - msg = "Problem successfully {action} for {attempted} students for problem '{problem}'" + msg = "Problem successfully {action} for {attempted} students" elif num_updated < num_attempted: - msg = "Problem {action} for {updated} of {attempted} students for problem '{problem}'" + msg = "Problem {action} for {updated} of {attempted} students" + + if course_task_log_entry.student is not None and num_attempted != num_total: + msg += " (out of {total})" # Update status in task result object itself: - message = msg.format(action=action_name, updated=num_updated, attempted=num_attempted, + message = msg.format(action=action_name, updated=num_updated, attempted=num_attempted, total=num_total, student=course_task_log_entry.student, problem=course_task_log_entry.task_args) return (succeeded, message) @@ -343,7 +348,7 @@ def submit_regrade_problem_for_student(request, course_id, problem_url, student) An exception is thrown if the problem doesn't exist, or if the particular problem is already being regraded for this student. """ - # check arguments: let exceptions return up to the caller. + # check arguments: let exceptions return up to the caller. _check_arguments_for_regrading(course_id, problem_url) task_name = 'regrade_problem' diff --git a/lms/djangoapps/courseware/tests/test_task_queue.py b/lms/djangoapps/courseware/tests/test_task_queue.py index 3a20fb237d..c1ae1925e1 100644 --- a/lms/djangoapps/courseware/tests/test_task_queue.py +++ b/lms/djangoapps/courseware/tests/test_task_queue.py @@ -225,7 +225,7 @@ class TaskQueueTestCase(TestCase): self.assertFalse(output['succeeded']) _, output = self._get_output_for_task_success(10, 0, 10) - self.assertTrue("Problem failed to be regraded for any of 10 students " in output['message']) + self.assertTrue("Problem failed to be regraded for any of 10 students" in output['message']) self.assertFalse(output['succeeded']) _, output = self._get_output_for_task_success(10, 8, 10) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 6698635d9a..cde47c4b7a 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -19,9 +19,12 @@ from django.contrib.auth.models import User, Group from django.http import HttpResponse from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control -from mitxmako.shortcuts import render_to_response from django.core.urlresolvers import reverse +import xmodule.graders as xmgraders +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError + from courseware import grades from courseware import task_queue from courseware.access import (has_access, get_access_group_name, @@ -33,14 +36,12 @@ from django_comment_common.models import (Role, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA) from django_comment_client.utils import has_forum_access +from instructor.offline_gradecalc import student_grades, offline_grades_available +from mitxmako.shortcuts import render_to_response from psychometrics import psychoanalyze from student.models import CourseEnrollment, CourseEnrollmentAllowed -from xmodule.modulestore.django import modulestore -import xmodule.graders as xmgraders import track.views -from .offline_gradecalc import student_grades, offline_grades_available -from xmodule.modulestore.exceptions import ItemNotFoundError log = logging.getLogger(__name__) @@ -156,6 +157,20 @@ def instructor_dashboard(request, course_id): (org, course_name, _) = course_id.split("/") return "i4x://" + org + "/" + course_name + "/" + urlname + def get_student_from_identifier(unique_student_identifier): + # try to uniquely id student by email address or username + msg = "" + try: + if "@" in unique_student_identifier: + student = User.objects.get(email=unique_student_identifier) + else: + student = User.objects.get(username=unique_student_identifier) + msg += "Found a single student. " + except User.DoesNotExist: + student = None + msg += "Couldn't find student with that email or username. " + return msg, student + # process actions from form POST action = request.POST.get('action', '') use_offline = request.POST.get('use_offline_grades', False) @@ -259,31 +274,49 @@ def instructor_dashboard(request, course_id): log.error("Encountered exception from reset: {0}".format(e)) msg += 'Failed to create a background task for resetting "{0}": {1}.'.format(problem_url, e.message) - elif "Reset student's attempts" in action or "Delete student state for module" in action \ + elif "Show Background Task History for Student" in action: + # put this before the non-student case, since the use of "in" will cause this to be missed + unique_student_identifier = request.POST.get('unique_student_identifier', '') + message, student = get_student_from_identifier(unique_student_identifier) + if student is None: + msg += message + else: + problem_urlname = request.POST.get('problem_for_student', '') + problem_url = get_module_url(problem_urlname) + message, task_datatable = get_background_task_table(course_id, problem_url, student) + msg += message + if task_datatable is not None: + datatable = task_datatable + datatable['title'] = "{course_id} > {location} > {student}".format(course_id=course_id, + location=problem_url, + student=student.username) + + elif "Show Background Task History" in action: + problem_urlname = request.POST.get('problem_for_all_students', '') + problem_url = get_module_url(problem_urlname) + message, task_datatable = get_background_task_table(course_id, problem_url) + msg += message + if task_datatable is not None: + datatable = task_datatable + datatable['title'] = "{course_id} > {location}".format(course_id=course_id, location=problem_url) + + elif "Reset student's attempts" in action \ + or "Delete student state for module" in action \ or "Regrade student's problem submission" in action: # get the form data unique_student_identifier = request.POST.get('unique_student_identifier', '') problem_urlname = request.POST.get('problem_for_student', '') module_state_key = get_module_url(problem_urlname) - # try to uniquely id student by email address or username - try: - if "@" in unique_student_identifier: - student = User.objects.get(email=unique_student_identifier) - else: - student = User.objects.get(username=unique_student_identifier) - msg += "Found a single student. " - except User.DoesNotExist: - student = None - msg += "Couldn't find student with that email or username. " - + message, student = get_student_from_identifier(unique_student_identifier) + msg += message student_module = None if student is not None: # find the module in question try: student_module = StudentModule.objects.get(student_id=student.id, - course_id=course_id, - module_state_key=module_state_key) + course_id=course_id, + module_state_key=module_state_key) msg += "Found module. " except StudentModule.DoesNotExist: msg += "Couldn't find module with that urlname. " @@ -336,22 +369,19 @@ def instructor_dashboard(request, course_id): elif "Get link to student's progress page" in action: unique_student_identifier = request.POST.get('unique_student_identifier', '') - try: - if "@" in unique_student_identifier: - student_to_reset = User.objects.get(email=unique_student_identifier) - else: - student_to_reset = User.objects.get(username=unique_student_identifier) - progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student_to_reset.id}) + # try to uniquely id student by email address or username + message, student = get_student_from_identifier(unique_student_identifier) + msg += message + if student is not None: + progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id}) track.views.server_track(request, '{instructor} requested progress page for {student} in {course}'.format( - student=student_to_reset, + student=student, instructor=request.user, course=course_id), {}, page='idashboard') - msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student_to_reset.username, student_to_reset.email) - except User.DoesNotExist: - msg += "Couldn't find student with that username. " + msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student.username, student.email) #---------------------------------------- # export grades to remote gradebook @@ -492,7 +522,7 @@ def instructor_dashboard(request, course_id): if problem_to_dump[-4:] == ".xml": problem_to_dump = problem_to_dump[:-4] try: - (org, course_name, run) = course_id.split("/") + (org, course_name, _) = course_id.split("/") module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_dump smdat = StudentModule.objects.filter(course_id=course_id, module_state_key=module_state_key) @@ -1251,99 +1281,56 @@ def dump_grading_context(course): return msg -#def old1testcelery(request): -# """ -# A Simple view that checks if the application can talk to the celery workers -# """ -# args = ('ping',) -# result = tasks.echo.apply_async(args, retry=False) -# value = result.get(timeout=0.5) -# output = { -# 'task_id': result.id, -# 'value': value -# } -# return HttpResponse(json.dumps(output, indent=4)) -# -# -#def old2testcelery(request): -# """ -# A Simple view that checks if the application can talk to the celery workers -# """ -# args = (10,) -# result = tasks.waitawhile.apply_async(args, retry=False) -# while not result.ready(): -# sleep(0.5) # in seconds -# if result.state == "PROGRESS": -# if hasattr(result, 'result') and 'current' in result.result: -# log.info("still waiting... progress at {0} of {1}".format(result.result['current'], result.result['total'])) -# else: -# log.info("still making progress... ") -# if result.successful(): -# value = result.result -# output = { -# 'task_id': result.id, -# 'value': value -# } -# return HttpResponse(json.dumps(output, indent=4)) -# -# -#def testcelery(request): -# """ -# A Simple view that checks if the application can talk to the celery workers -# """ -# args = (10,) -# result = tasks.waitawhile.apply_async(args, retry=False) -# task_id = result.id -# # return the task_id to a template which will set up an ajax call to -# # check the progress of the task. -# return testcelery_status(request, task_id) -## return mitxmako.shortcuts.render_to_response('celery_ajax.html', { -## 'element_id': 'celery_task' -## 'id': self.task_id, -## 'ajax_url': reverse('testcelery_ajax'), -## }) -# -# -#def testcelery_status(request, task_id): -# result = tasks.waitawhile.AsyncResult(task_id) -# while not result.ready(): -# sleep(0.5) # in seconds -# if result.state == "PROGRESS": -# if hasattr(result, 'result') and 'current' in result.result: -# log.info("still waiting... progress at {0} of {1}".format(result.result['current'], result.result['total'])) -# else: -# log.info("still making progress... ") -# if result.successful(): -# value = result.result -# output = { -# 'task_id': result.id, -# 'value': value -# } -# return HttpResponse(json.dumps(output, indent=4)) -# -# -#def celery_task_status(request, task_id): -# # TODO: determine if we need to know the name of the original task, -# # or if this could be any task... Sample code seems to indicate that -# # we could just include the AsyncResult class directly, i.e.: -# # from celery.result import AsyncResult. -# result = tasks.waitawhile.AsyncResult(task_id) -# -# output = { -# 'task_id': result.id, -# 'state': result.state -# } -# -# if result.state == "PROGRESS": -# if hasattr(result, 'result') and 'current' in result.result: -# log.info("still waiting... progress at {0} of {1}".format(result.result['current'], result.result['total'])) -# output['current'] = result.result['current'] -# output['total'] = result.result['total'] -# else: -# log.info("still making progress... ") -# -# if result.successful(): -# value = result.result -# output['value'] = value -# -# return HttpResponse(json.dumps(output, indent=4)) +def get_background_task_table(course_id, problem_url, student=None): + course_tasks = CourseTaskLog.objects.filter(course_id=course_id, task_args=problem_url) + if student is not None: + course_tasks = course_tasks.filter(student=student) + + history_entries = course_tasks.order_by('-id') + datatable = None + msg = "" + # first check to see if there is any history at all + # (note that we don't have to check that the arguments are valid; it + # just won't find any entries.) + if (len(history_entries)) == 0: + if student is not None: + log.debug("Found no background tasks for request: {course}, {problem}, and student {student}".format(course=course_id, problem=problem_url, student=student.username)) + template = 'Failed to find any background tasks for course "{course}", module "{problem}" and student "{student}".' + msg += template.format(course=course_id, problem=problem_url, student=student.username) + else: + log.debug("Found no background tasks for request: {course}, {problem}".format(course=course_id, problem=problem_url)) + msg += 'Failed to find any background tasks for course "{course}" and module "{problem}".'.format(course=course_id, problem=problem_url) + else: + datatable = {} + datatable['header'] = ["Order", + "Task Name", + "Student", + "Task Id", + "Requester", + "Submitted", + "Updated", + "Task State", + "Task Status", + "Message"] + + datatable['data'] = [] + for i, course_task in enumerate(history_entries): + success, message = task_queue.get_task_completion_message(course_task) + if success: + status = "Complete" + else: + status = "Incomplete" + row = ["#{0}".format(len(history_entries) - i), + str(course_task.task_name), + str(course_task.student), + str(course_task.task_id), + str(course_task.requester), + course_task.created.strftime("%Y/%m/%d %H:%M:%S"), + course_task.updated.strftime("%Y/%m/%d %H:%M:%S"), + str(course_task.task_state), + status, + message] + datatable['data'].append(row) + + return msg, datatable +