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.
This commit is contained in:
@@ -1,43 +1,31 @@
|
||||
|
||||
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):
|
||||
@@ -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>
|
||||
|
||||
@@ -384,6 +488,43 @@ function goto( mode)
|
||||
%if msg:
|
||||
<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
|
||||
|
||||
Reference in New Issue
Block a user