Commit d503e331 by Brian Wilson

Add task progress table to instructor dash.

Add call to MakoMiddleware() to initialize templates in celery worker server.

Pass task_ids by POST properties (as a list) to collect task progress status.
parent 91ac6e68
import json import json
import logging import logging
from time import sleep
from django.contrib.auth.models import User from django.contrib.auth.models import User
import mitxmako.middleware as middleware
from django.http import HttpResponse
# from django.http import HttpRequest
from django.test.client import RequestFactory
from celery import task, current_task
from celery.result import AsyncResult
from celery.utils.log import get_task_logger
from courseware.models import StudentModule, CourseTaskLog from courseware.models import StudentModule, CourseTaskLog
from courseware.model_data import ModelDataCache from courseware.model_data import ModelDataCache
from courseware.module_render import get_module from courseware.module_render import get_module
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError,\ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
InvalidLocationError
import track.views import track.views
from celery import task, current_task
from celery.utils.log import get_task_logger
from time import sleep
from django.core.handlers.wsgi import WSGIRequest
# define different loggers for use within tasks and on client side
logger = get_task_logger(__name__) logger = get_task_logger(__name__)
# celery = Celery('tasks', broker='django://')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@task @task
def add(x, y):
return x + y
@task
def echo(value):
if value == 'ping':
result = 'pong'
else:
result = 'got: {0}'.format(value)
return result
@task
def waitawhile(value): def waitawhile(value):
for i in range(value): for i in range(value):
sleep(1) # in seconds sleep(1) # in seconds
...@@ -66,6 +54,15 @@ def _update_problem_module_state(request, course_id, problem_url, student, updat ...@@ -66,6 +54,15 @@ def _update_problem_module_state(request, course_id, problem_url, student, updat
# complete, as far as celery is concerned, but have an internal status of failed.) # complete, as far as celery is concerned, but have an internal status of failed.)
succeeded = False succeeded = False
# add hack so that mako templates will work on celery worker server:
# The initialization of Make templating is usually done when Django is
# initialize middleware packages as part of processing a server request.
# When this is run on a celery worker server, no such initialization is
# called. So we look for the result: the defining of the lookup paths
# for templates.
if 'main' not in middleware.lookup:
middleware.MakoMiddleware()
# find the problem descriptor, if any: # find the problem descriptor, if any:
try: try:
module_descriptor = modulestore().get_instance(course_id, module_state_key) module_descriptor = modulestore().get_instance(course_id, module_state_key)
...@@ -105,8 +102,8 @@ def _update_problem_module_state(request, course_id, problem_url, student, updat ...@@ -105,8 +102,8 @@ def _update_problem_module_state(request, course_id, problem_url, student, updat
# try: # try:
if update_fcn(request, module_to_update, module_descriptor): if update_fcn(request, module_to_update, module_descriptor):
num_updated += 1 num_updated += 1
# if there's an error, just let it throw, and the task will # if there's an error, just let it throw, and the task will
# be marked as FAILED, with a stack trace. # be marked as FAILED, with a stack trace.
# except UpdateProblemModuleStateError as e: # except UpdateProblemModuleStateError as e:
# something bad happened, so exit right away # something bad happened, so exit right away
# return (succeeded, e.message) # return (succeeded, e.message)
...@@ -142,7 +139,8 @@ def _update_problem_module_state(request, course_id, problem_url, student, updat ...@@ -142,7 +139,8 @@ def _update_problem_module_state(request, course_id, problem_url, student, updat
# and update status in course task table as well: # and update status in course task table as well:
# TODO: figure out how this is legal. The actual task result # TODO: figure out how this is legal. The actual task result
# status is updated by celery when this task completes, and is # status is updated by celery when this task completes, and is
# not # presumably going to clobber this custom metadata. So if we want
# any such status to persist, we have to write it to the CourseTaskLog instead.
# course_task_log_entry = CourseTaskLog.objects.get(task_id=current_task.id) # course_task_log_entry = CourseTaskLog.objects.get(task_id=current_task.id)
# course_task_log_entry.task_status = ... # course_task_log_entry.task_status = ...
...@@ -216,10 +214,9 @@ def _regrade_problem_module_state(request, module_to_regrade, module_descriptor) ...@@ -216,10 +214,9 @@ def _regrade_problem_module_state(request, module_to_regrade, module_descriptor)
return False return False
else: else:
track.views.server_track(request, track.views.server_track(request,
'{instructor} regrade problem {problem} for student {student} ' 'regrade problem {problem} for student {student} '
'in {course}'.format(student=student.id, 'in {course}'.format(student=student.id,
problem=module_to_regrade.module_state_key, problem=module_to_regrade.module_state_key,
instructor=request.user,
course=course_id), course=course_id),
{}, {},
page='idashboard') page='idashboard')
...@@ -257,8 +254,10 @@ def regrade_problem_for_student(request, course_id, problem_url, student_identif ...@@ -257,8 +254,10 @@ def regrade_problem_for_student(request, course_id, problem_url, student_identif
@task @task
def _regrade_problem_for_all_students(request_environ, course_id, problem_url): def _regrade_problem_for_all_students(request_environ, course_id, problem_url):
# request = dummy_request # request = HttpRequest()
request = WSGIRequest(request_environ) # request.META.update(request_environ)
factory = RequestFactory(**request_environ)
request = factory.get('/')
action_name = 'regraded' action_name = 'regraded'
update_fcn = _regrade_problem_module_state update_fcn = _regrade_problem_module_state
filter_fcn = filter_problem_module_state_for_done filter_fcn = filter_problem_module_state_for_done
...@@ -269,6 +268,7 @@ def _regrade_problem_for_all_students(request_environ, course_id, problem_url): ...@@ -269,6 +268,7 @@ def _regrade_problem_for_all_students(request_environ, course_id, problem_url):
def regrade_problem_for_all_students(request, course_id, problem_url): def regrade_problem_for_all_students(request, course_id, problem_url):
# Figure out (for now) how to serialize what we need of the request. The actual # Figure out (for now) how to serialize what we need of the request. The actual
# request will not successfully serialize with json or with pickle. # request will not successfully serialize with json or with pickle.
# Maybe we can just pass all META info as a dict.
request_environ = {'HTTP_USER_AGENT': request.META['HTTP_USER_AGENT'], request_environ = {'HTTP_USER_AGENT': request.META['HTTP_USER_AGENT'],
'REMOTE_ADDR': request.META['REMOTE_ADDR'], 'REMOTE_ADDR': request.META['REMOTE_ADDR'],
'SERVER_NAME': request.META['SERVER_NAME'], 'SERVER_NAME': request.META['SERVER_NAME'],
...@@ -290,6 +290,76 @@ def regrade_problem_for_all_students(request, course_id, problem_url): ...@@ -290,6 +290,76 @@ def regrade_problem_for_all_students(request, course_id, problem_url):
return course_task_log return course_task_log
def course_task_log_status(request, task_id=None):
"""
This returns the status of a course-related task as a JSON-serialized dict.
"""
output = {}
if task_id is not None:
output = _get_course_task_log_status(task_id)
elif 'task_id' in request.POST:
task_id = request.POST['task_id']
output = _get_course_task_log_status(task_id)
elif 'task_ids[]' in request.POST:
tasks = request.POST.getlist('task_ids[]')
for task_id in tasks:
task_output = _get_course_task_log_status(task_id)
output[task_id] = task_output
# TODO else: raise exception?
return HttpResponse(json.dumps(output, indent=4))
def _get_course_task_log_status(task_id):
course_task_log_entry = CourseTaskLog.objects.get(task_id=task_id)
# TODO: error handling if it doesn't exist...
def not_in_progress(entry):
# TODO: do better than to copy list from celery.states.READY_STATES
return entry.task_status in ['SUCCESS', 'FAILURE', 'REVOKED']
# if the task is already known to be done, then there's no reason to query
# the underlying task:
if not_in_progress(course_task_log_entry):
output = {
'task_id': course_task_log_entry.task_id,
'task_status': course_task_log_entry.task_status,
'in_progress': False
}
return output
# we need to get information from the task result directly now.
result = AsyncResult(task_id)
output = {
'task_id': result.id,
'task_status': result.state,
'in_progress': True
}
if result.traceback is not None:
output['task_traceback'] = result.traceback
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
# update the entry if necessary:
if course_task_log_entry.task_status != result.state:
course_task_log_entry.task_status = result.state
course_task_log_entry.save()
return output
def _reset_problem_attempts_module_state(request, module_to_reset, module_descriptor): def _reset_problem_attempts_module_state(request, module_to_reset, module_descriptor):
# modify the problem's state # modify the problem's state
# load the state json and change state # load the state json and change state
......
...@@ -29,7 +29,7 @@ from courseware import tasks ...@@ -29,7 +29,7 @@ from courseware import tasks
from courseware.access import (has_access, get_access_group_name, from courseware.access import (has_access, get_access_group_name,
course_beta_test_group_name) course_beta_test_group_name)
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from courseware.models import StudentModule from courseware.models import StudentModule, CourseTaskLog
from django_comment_common.models import (Role, from django_comment_common.models import (Role,
FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_MODERATOR, FORUM_ROLE_MODERATOR,
...@@ -217,10 +217,13 @@ def instructor_dashboard(request, course_id): ...@@ -217,10 +217,13 @@ def instructor_dashboard(request, course_id):
elif "Regrade ALL students' problem submissions" in action: elif "Regrade ALL students' problem submissions" in action:
problem_url = request.POST.get('problem_to_regrade', '') problem_url = request.POST.get('problem_to_regrade', '')
try: try:
result = tasks.regrade_problem_for_all_students(request, course_id, problem_url) course_task_log_entry = tasks.regrade_problem_for_all_students(request, course_id, problem_url)
except Exception as e: except Exception as e:
log.error("Encountered exception from regrade: {msg}", msg=e.message()) log.error("Encountered exception from regrade: {0}", e)
# check that a course_task_log entry was created:
if course_task_log_entry is None:
msg += '<font="red">Failed to create a background task for regrading "{0}".</font>'.format(problem_url)
elif "Reset student's attempts" in action or "Delete student state for problem" in action: elif "Reset student's attempts" in action or "Delete student state for problem" 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', '')
...@@ -641,6 +644,9 @@ def instructor_dashboard(request, course_id): ...@@ -641,6 +644,9 @@ def instructor_dashboard(request, course_id):
if use_offline: if use_offline:
msg += "<br/><font color='orange'>Grades from %s</font>" % offline_grades_available(course_id) msg += "<br/><font color='orange'>Grades from %s</font>" % offline_grades_available(course_id)
# generate list of pending background tasks
course_tasks = CourseTaskLog.objects.filter(course_id = course_id).exclude(task_status='SUCCESS').exclude(task_status='FAILURE')
#---------------------------------------- #----------------------------------------
# context for rendering # context for rendering
...@@ -655,7 +661,7 @@ def instructor_dashboard(request, course_id): ...@@ -655,7 +661,7 @@ def instructor_dashboard(request, course_id):
'problems': problems, # psychometrics 'problems': problems, # psychometrics
'plots': plots, # psychometrics 'plots': plots, # psychometrics
'course_errors': modulestore().get_item_errors(course.location), 'course_errors': modulestore().get_item_errors(course.location),
'course_tasks': course_tasks,
'djangopid': os.getpid(), 'djangopid': os.getpid(),
'mitx_version': getattr(settings, 'MITX_VERSION_STRING', ''), 'mitx_version': getattr(settings, 'MITX_VERSION_STRING', ''),
'offline_grade_log': offline_grades_available(course_id), 'offline_grade_log': offline_grades_available(course_id),
......
...@@ -9,6 +9,110 @@ ...@@ -9,6 +9,110 @@
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-world-mill-en.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-world-mill-en.js')}"></script>
<script type="text/javascript" src="${static.url('js/course_groups/cohorts.js')}"></script> <script type="text/javascript" src="${static.url('js/course_groups/cohorts.js')}"></script>
%if course_tasks is not None:
<script type="text/javascript">
(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.CourseTaskProgress = (function() {
function CourseTaskProgress(element) {
this.poll = __bind(this.poll, this);
this.queueing = __bind(this.queueing, this);
this.element = element;
this.reinitialize(element);
// start the work here
this.queueing();
}
CourseTaskProgress.prototype.reinitialize = function(element) {
this.entries = $(element).find('.task-progress-entry')
};
CourseTaskProgress.prototype.$ = function(selector) {
return $(selector, this.element);
};
CourseTaskProgress.prototype.queueing = function() {
if (window.queuePollerID) {
window.clearTimeout(window.queuePollerID);
}
return window.queuePollerID = window.setTimeout(this.poll, 1000);
};
CourseTaskProgress.prototype.poll = function() {
var _this = this;
// clear the array of entries to poll this time
this.task_ids = [];
// then go through the entries, update each,
// and decide if it should go onto the next list
this.entries.each(function(idx, element) {
var task_id = $(element).data('taskId');
_this.task_ids.push(task_id);
});
var ajax_url = '/course_task_log_status/';
// Note that the keyname here ends up with "[]" being appended
// in the POST parameter that shows up on the Django server.
var data = {'task_ids': this.task_ids };
// TODO: split callback out into a separate function defn.
$.post(ajax_url, data).done(function(response) {
// expect to receive a dict with an entry for each
// requested task_id.
// Each should indicate if it were in_progress.
// If none are, then delete the poller.
// If any are, add them to the list of entries to
// be requeried, and reset the timer to call this
// again.
// TODO: clean out _this.entries, and add back
// only those entries that are still pending.
var something_in_progress = false;
for (name in response) {
if (response.hasOwnProperty(name)) {
var task_id = name;
var task_dict = response[task_id];
// this should be a dict of properties for this task_id
var in_progress = task_dict.in_progress
if (in_progress === true) {
something_in_progress = true;
}
// find the corresponding entry, and update it:
selector = '[data-task-id="' + task_id + '"]';
entry = $(_this.element).find(selector);
var task_status_el = entry.find('.task-status');
task_status_el.text(task_dict.task_status)
var task_progress_el = entry.find('.task-progress');
var progress_value = task_dict.task_progress || '';
task_progress_el.text(progress_value);
}
}
if (something_in_progress) {
// TODO: set the refresh interval. (And if it is disabled,
// then don't set the timeout at all.)
return window.queuePollerID = window.setTimeout(_this.poll, 1000);
} else {
delete window.queuePollerID;
}
});
};
return CourseTaskProgress;
})();
}).call(this);
// once the page is rendered, create the progress object
var courseTaskProgress;
$(document).ready(function() {
courseTaskProgress = new CourseTaskProgress($('#task-progress-wrapper'));
});
</script>
%endif
</%block> </%block>
...@@ -385,6 +489,43 @@ function goto( mode) ...@@ -385,6 +489,43 @@ function goto( mode)
<p></p><p>${msg}</p> <p></p><p>${msg}</p>
%endif %endif
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
## Output tasks in progress
%if course_tasks is not None:
<p>Pending Course Tasks</p>
<div id="task-progress-wrapper">
<table class="stat_table">
<tr>
<th>Task Name</th>
<th>Task Arg</th>
<th>Student</th>
<th>Task Id</th>
<th>Requester</th>
<th>Submitted</th>
<th>Last Update</th>
<th>Task Status</th>
<th>Task Progress</th>
</tr>
%for tasknum, course_task in enumerate(course_tasks):
<tr id="task-progress-entry-${tasknum}" class="task-progress-entry" data-task-id="${course_task.task_id}">
<td>${course_task.task_name}</td>
<td>${course_task.task_args}</td>
<td>${course_task.student}</td>
<td><div class="task-id">${course_task.task_id}</div></td>
<td>${course_task.requester}</td>
<td>${course_task.created}</td>
<td><div class="task-updated">${course_task.updated}</div></td>
<td><div class="task-status">${course_task.task_status}</div></td>
<td><div class="task-progress">unknown</div></td>
</tr>
%endfor
</table>
</div>
<br/>
%endif
##-----------------------------------------------------------------------------
%if modeflag.get('Analytics'): %if modeflag.get('Analytics'):
......
...@@ -58,6 +58,7 @@ urlpatterns = ('', # nopep8 ...@@ -58,6 +58,7 @@ urlpatterns = ('', # nopep8
name='auth_password_reset_done'), name='auth_password_reset_done'),
url(r'^heartbeat$', include('heartbeat.urls')), url(r'^heartbeat$', include('heartbeat.urls')),
url(r'^course_task_log_status/$', 'courseware.tasks.course_task_log_status', name='course_task_log_status'),
) )
# University profiles only make sense in the default edX context # University profiles only make sense in the default edX context
......
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