Add background task history to instructor dash (as table).
Add task-history-per-student button, and fix display of task messages.
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 += "<font color='red'>Couldn't find student with that email or username. </font>"
|
||||
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 += '<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:
|
||||
# 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 += "<font color='red'>Couldn't find student with that email or username. </font>"
|
||||
|
||||
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 += "<font color='red'>Couldn't find module with that urlname. </font>"
|
||||
@@ -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 += "<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)
|
||||
except User.DoesNotExist:
|
||||
msg += "<font color='red'>Couldn't find student with that username. </font>"
|
||||
msg += "<a href='{0}' target='_blank'> Progress page for username: {1} with email address: {2}</a>.".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 = '<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)
|
||||
else:
|
||||
log.debug("Found no background tasks for request: {course}, {problem}".format(course=course_id, problem=problem_url))
|
||||
msg += '<font color="red">Failed to find any background tasks for course "{course}" and module "{problem}".</font>'.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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user