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 logging
from time import sleep
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.model_data import ModelDataCache
from courseware.module_render import get_module
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError,\
InvalidLocationError
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
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__)
# celery = Celery('tasks', broker='django://')
log = logging.getLogger(__name__)
@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):
for i in range(value):
sleep(1) # in seconds
......@@ -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.)
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:
try:
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
# try:
if update_fcn(request, module_to_update, module_descriptor):
num_updated += 1
# if there's an error, just let it throw, and the task will
# be marked as FAILED, with a stack trace.
# if there's an error, just let it throw, and the task will
# be marked as FAILED, with a stack trace.
# except UpdateProblemModuleStateError as e:
# something bad happened, so exit right away
# return (succeeded, e.message)
......@@ -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:
# TODO: figure out how this is legal. The actual task result
# 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.task_status = ...
......@@ -216,10 +214,9 @@ def _regrade_problem_module_state(request, module_to_regrade, module_descriptor)
return False
else:
track.views.server_track(request,
'{instructor} regrade problem {problem} for student {student} '
'regrade problem {problem} for student {student} '
'in {course}'.format(student=student.id,
problem=module_to_regrade.module_state_key,
instructor=request.user,
course=course_id),
{},
page='idashboard')
......@@ -257,8 +254,10 @@ def regrade_problem_for_student(request, course_id, problem_url, student_identif
@task
def _regrade_problem_for_all_students(request_environ, course_id, problem_url):
# request = dummy_request
request = WSGIRequest(request_environ)
# request = HttpRequest()
# request.META.update(request_environ)
factory = RequestFactory(**request_environ)
request = factory.get('/')
action_name = 'regraded'
update_fcn = _regrade_problem_module_state
filter_fcn = filter_problem_module_state_for_done
......@@ -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):
# 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.
# Maybe we can just pass all META info as a dict.
request_environ = {'HTTP_USER_AGENT': request.META['HTTP_USER_AGENT'],
'REMOTE_ADDR': request.META['REMOTE_ADDR'],
'SERVER_NAME': request.META['SERVER_NAME'],
......@@ -290,6 +290,76 @@ def regrade_problem_for_all_students(request, course_id, problem_url):
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):
# modify the problem's state
# load the state json and change state
......
......@@ -29,7 +29,7 @@ from courseware import tasks
from courseware.access import (has_access, get_access_group_name,
course_beta_test_group_name)
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,
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_MODERATOR,
......@@ -217,10 +217,13 @@ def instructor_dashboard(request, course_id):
elif "Regrade ALL students' problem submissions" in action:
problem_url = request.POST.get('problem_to_regrade', '')
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:
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:
# get the form data
unique_student_identifier = request.POST.get('unique_student_identifier', '')
......@@ -641,6 +644,9 @@ def instructor_dashboard(request, course_id):
if use_offline:
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
......@@ -655,7 +661,7 @@ def instructor_dashboard(request, course_id):
'problems': problems, # psychometrics
'plots': plots, # psychometrics
'course_errors': modulestore().get_item_errors(course.location),
'course_tasks': course_tasks,
'djangopid': os.getpid(),
'mitx_version': getattr(settings, 'MITX_VERSION_STRING', ''),
'offline_grade_log': offline_grades_available(course_id),
......
......@@ -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-world-mill-en.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>
......@@ -385,6 +489,43 @@ function goto( mode)
<p></p><p>${msg}</p>
%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'):
......
......@@ -58,6 +58,7 @@ urlpatterns = ('', # nopep8
name='auth_password_reset_done'),
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
......
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