Commit 297206f2 by Brian Wilson

Add background task history to instructor dash (as table).

Add task-history-per-student button, and fix display of task messages.
parent 6cd23875
...@@ -143,6 +143,8 @@ def _update_course_task_log(course_task_log_entry, task_result): ...@@ -143,6 +143,8 @@ def _update_course_task_log(course_task_log_entry, task_result):
Calculates json to store in task_progress field. 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 task_id = task_result.task_id
result_state = task_result.state result_state = task_result.state
returned_result = task_result.result returned_result = task_result.result
...@@ -240,39 +242,36 @@ def _get_course_task_log_status(task_id): ...@@ -240,39 +242,36 @@ def _get_course_task_log_status(task_id):
# define ajax return value: # define ajax return value:
output = {} 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: # the underlying task's result object:
if course_task_log_entry.task_state not in READY_STATES: 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) result = AsyncResult(task_id)
output.update(_update_course_task_log(course_task_log_entry, result)) output.update(_update_course_task_log(course_task_log_entry, result))
elif course_task_log_entry.task_progress is not None: elif course_task_log_entry.task_progress is not None:
# task is already known to have finished, but report on its status: # task is already known to have finished, but report on its status:
output['task_progress'] = json.loads(course_task_log_entry.task_progress) 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 basic information matching what's stored in CourseTaskLog:
output['task_id'] = course_task_log_entry.task_id output['task_id'] = course_task_log_entry.task_id
output['task_state'] = course_task_log_entry.task_state output['task_state'] = course_task_log_entry.task_state
output['in_progress'] = course_task_log_entry.task_state not in READY_STATES output['in_progress'] = course_task_log_entry.task_state not in READY_STATES
if course_task_log_entry.task_state == 'SUCCESS': if course_task_log_entry.task_state in READY_STATES:
succeeded, message = _get_task_completion_message(course_task_log_entry) succeeded, message = get_task_completion_message(course_task_log_entry)
output['message'] = message output['message'] = message
output['succeeded'] = succeeded output['succeeded'] = succeeded
return output 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. Construct progress message from progress information in CourseTaskLog entry.
Returns (boolean, message string) duple. 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 succeeded = False
...@@ -281,30 +280,36 @@ def _get_task_completion_message(course_task_log_entry): ...@@ -281,30 +280,36 @@ def _get_task_completion_message(course_task_log_entry):
return (succeeded, "No status information available") return (succeeded, "No status information available")
task_progress = json.loads(course_task_log_entry.task_progress) 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'] action_name = task_progress['action_name']
num_attempted = task_progress['attempted'] num_attempted = task_progress['attempted']
num_updated = task_progress['updated'] 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 course_task_log_entry.student is not None:
if num_attempted == 0: 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: 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: else:
succeeded = True succeeded = True
msg = "Problem successfully {action} for student '{student}' and problem '{problem}'" msg = "Problem successfully {action} for student '{student}'"
elif num_attempted == 0: 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: 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: elif num_updated == num_attempted:
succeeded = True succeeded = True
msg = "Problem successfully {action} for {attempted} students for problem '{problem}'" msg = "Problem successfully {action} for {attempted} students"
elif num_updated < num_attempted: 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: # 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) student=course_task_log_entry.student, problem=course_task_log_entry.task_args)
return (succeeded, message) return (succeeded, message)
...@@ -343,7 +348,7 @@ def submit_regrade_problem_for_student(request, course_id, problem_url, student) ...@@ -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 An exception is thrown if the problem doesn't exist, or if the particular
problem is already being regraded for this student. 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) _check_arguments_for_regrading(course_id, problem_url)
task_name = 'regrade_problem' task_name = 'regrade_problem'
......
...@@ -225,7 +225,7 @@ class TaskQueueTestCase(TestCase): ...@@ -225,7 +225,7 @@ class TaskQueueTestCase(TestCase):
self.assertFalse(output['succeeded']) self.assertFalse(output['succeeded'])
_, output = self._get_output_for_task_success(10, 0, 10) _, 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']) self.assertFalse(output['succeeded'])
_, output = self._get_output_for_task_success(10, 8, 10) _, output = self._get_output_for_task_success(10, 8, 10)
......
...@@ -19,9 +19,12 @@ from django.contrib.auth.models import User, Group ...@@ -19,9 +19,12 @@ from django.contrib.auth.models import User, Group
from django.http import HttpResponse from django.http import HttpResponse
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from mitxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse 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 grades
from courseware import task_queue from courseware import task_queue
from courseware.access import (has_access, get_access_group_name, from courseware.access import (has_access, get_access_group_name,
...@@ -33,14 +36,12 @@ from django_comment_common.models import (Role, ...@@ -33,14 +36,12 @@ from django_comment_common.models import (Role,
FORUM_ROLE_MODERATOR, FORUM_ROLE_MODERATOR,
FORUM_ROLE_COMMUNITY_TA) FORUM_ROLE_COMMUNITY_TA)
from django_comment_client.utils import has_forum_access 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 psychometrics import psychoanalyze
from student.models import CourseEnrollment, CourseEnrollmentAllowed from student.models import CourseEnrollment, CourseEnrollmentAllowed
from xmodule.modulestore.django import modulestore
import xmodule.graders as xmgraders
import track.views import track.views
from .offline_gradecalc import student_grades, offline_grades_available
from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -156,6 +157,20 @@ def instructor_dashboard(request, course_id): ...@@ -156,6 +157,20 @@ def instructor_dashboard(request, course_id):
(org, course_name, _) = course_id.split("/") (org, course_name, _) = course_id.split("/")
return "i4x://" + org + "/" + course_name + "/" + urlname 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 += "<font color='red'>Couldn't find student with that email or username. </font>"
return msg, student
# process actions from form POST # process actions from form POST
action = request.POST.get('action', '') action = request.POST.get('action', '')
use_offline = request.POST.get('use_offline_grades', False) use_offline = request.POST.get('use_offline_grades', False)
...@@ -259,31 +274,49 @@ def instructor_dashboard(request, course_id): ...@@ -259,31 +274,49 @@ def instructor_dashboard(request, course_id):
log.error("Encountered exception from reset: {0}".format(e)) log.error("Encountered exception from reset: {0}".format(e))
msg += '<font color="red">Failed to create a background task for resetting "{0}": {1}.</font>'.format(problem_url, e.message) msg += '<font color="red">Failed to create a background task for resetting "{0}": {1}.</font>'.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: or "Regrade student's problem submission" in action:
# get the form data # get the form data
unique_student_identifier = request.POST.get('unique_student_identifier', '') unique_student_identifier = request.POST.get('unique_student_identifier', '')
problem_urlname = request.POST.get('problem_for_student', '') problem_urlname = request.POST.get('problem_for_student', '')
module_state_key = get_module_url(problem_urlname) module_state_key = get_module_url(problem_urlname)
# try to uniquely id student by email address or username # try to uniquely id student by email address or username
try: message, student = get_student_from_identifier(unique_student_identifier)
if "@" in unique_student_identifier: msg += message
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 += "<font color='red'>Couldn't find student with that email or username. </font>"
student_module = None student_module = None
if student is not None: if student is not None:
# find the module in question # find the module in question
try: try:
student_module = StudentModule.objects.get(student_id=student.id, student_module = StudentModule.objects.get(student_id=student.id,
course_id=course_id, course_id=course_id,
module_state_key=module_state_key) module_state_key=module_state_key)
msg += "Found module. " msg += "Found module. "
except StudentModule.DoesNotExist: except StudentModule.DoesNotExist:
msg += "<font color='red'>Couldn't find module with that urlname. </font>" msg += "<font color='red'>Couldn't find module with that urlname. </font>"
...@@ -336,22 +369,19 @@ def instructor_dashboard(request, course_id): ...@@ -336,22 +369,19 @@ def instructor_dashboard(request, course_id):
elif "Get link to student's progress page" in action: elif "Get link to student's progress page" in action:
unique_student_identifier = request.POST.get('unique_student_identifier', '') unique_student_identifier = request.POST.get('unique_student_identifier', '')
try: # try to uniquely id student by email address or username
if "@" in unique_student_identifier: message, student = get_student_from_identifier(unique_student_identifier)
student_to_reset = User.objects.get(email=unique_student_identifier) msg += message
else: if student is not None:
student_to_reset = User.objects.get(username=unique_student_identifier) progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id})
progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student_to_reset.id})
track.views.server_track(request, track.views.server_track(request,
'{instructor} requested progress page for {student} in {course}'.format( '{instructor} requested progress page for {student} in {course}'.format(
student=student_to_reset, student=student,
instructor=request.user, instructor=request.user,
course=course_id), course=course_id),
{}, {},
page='idashboard') page='idashboard')
msg += "<a href='{0}' target='_blank'> Progress page for username: {1} with email address: {2}</a>.".format(progress_url, student_to_reset.username, student_to_reset.email) msg += "<a href='{0}' target='_blank'> Progress page for username: {1} with email address: {2}</a>.".format(progress_url, student.username, student.email)
except User.DoesNotExist:
msg += "<font color='red'>Couldn't find student with that username. </font>"
#---------------------------------------- #----------------------------------------
# export grades to remote gradebook # export grades to remote gradebook
...@@ -492,7 +522,7 @@ def instructor_dashboard(request, course_id): ...@@ -492,7 +522,7 @@ def instructor_dashboard(request, course_id):
if problem_to_dump[-4:] == ".xml": if problem_to_dump[-4:] == ".xml":
problem_to_dump = problem_to_dump[:-4] problem_to_dump = problem_to_dump[:-4]
try: 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 module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_dump
smdat = StudentModule.objects.filter(course_id=course_id, smdat = StudentModule.objects.filter(course_id=course_id,
module_state_key=module_state_key) module_state_key=module_state_key)
...@@ -1251,99 +1281,56 @@ def dump_grading_context(course): ...@@ -1251,99 +1281,56 @@ def dump_grading_context(course):
return msg return msg
#def old1testcelery(request): def get_background_task_table(course_id, problem_url, student=None):
# """ course_tasks = CourseTaskLog.objects.filter(course_id=course_id, task_args=problem_url)
# A Simple view that checks if the application can talk to the celery workers if student is not None:
# """ course_tasks = course_tasks.filter(student=student)
# args = ('ping',)
# result = tasks.echo.apply_async(args, retry=False) history_entries = course_tasks.order_by('-id')
# value = result.get(timeout=0.5) datatable = None
# output = { msg = ""
# 'task_id': result.id, # first check to see if there is any history at all
# 'value': value # (note that we don't have to check that the arguments are valid; it
# } # just won't find any entries.)
# return HttpResponse(json.dumps(output, indent=4)) 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))
#def old2testcelery(request): template = '<font color="red">Failed to find any background tasks for course "{course}", module "{problem}" and student "{student}".</font>'
# """ msg += template.format(course=course_id, problem=problem_url, student=student.username)
# A Simple view that checks if the application can talk to the celery workers else:
# """ log.debug("Found no background tasks for request: {course}, {problem}".format(course=course_id, problem=problem_url))
# args = (10,) msg += '<font color="red">Failed to find any background tasks for course "{course}" and module "{problem}".</font>'.format(course=course_id, problem=problem_url)
# result = tasks.waitawhile.apply_async(args, retry=False) else:
# while not result.ready(): datatable = {}
# sleep(0.5) # in seconds datatable['header'] = ["Order",
# if result.state == "PROGRESS": "Task Name",
# if hasattr(result, 'result') and 'current' in result.result: "Student",
# log.info("still waiting... progress at {0} of {1}".format(result.result['current'], result.result['total'])) "Task Id",
# else: "Requester",
# log.info("still making progress... ") "Submitted",
# if result.successful(): "Updated",
# value = result.result "Task State",
# output = { "Task Status",
# 'task_id': result.id, "Message"]
# 'value': value
# } datatable['data'] = []
# return HttpResponse(json.dumps(output, indent=4)) for i, course_task in enumerate(history_entries):
# success, message = task_queue.get_task_completion_message(course_task)
# if success:
#def testcelery(request): status = "Complete"
# """ else:
# A Simple view that checks if the application can talk to the celery workers status = "Incomplete"
# """ row = ["#{0}".format(len(history_entries) - i),
# args = (10,) str(course_task.task_name),
# result = tasks.waitawhile.apply_async(args, retry=False) str(course_task.student),
# task_id = result.id str(course_task.task_id),
# # return the task_id to a template which will set up an ajax call to str(course_task.requester),
# # check the progress of the task. course_task.created.strftime("%Y/%m/%d %H:%M:%S"),
# return testcelery_status(request, task_id) course_task.updated.strftime("%Y/%m/%d %H:%M:%S"),
## return mitxmako.shortcuts.render_to_response('celery_ajax.html', { str(course_task.task_state),
## 'element_id': 'celery_task' status,
## 'id': self.task_id, message]
## 'ajax_url': reverse('testcelery_ajax'), datatable['data'].append(row)
## })
# return msg, datatable
#
#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))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment