From d503e3313f0031e995bed1528241ca18ec422d9f Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 30 Apr 2013 02:16:15 -0400 Subject: [PATCH] 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. --- lms/djangoapps/courseware/tasks.py | 132 ++++++++++++---- lms/djangoapps/instructor/views.py | 16 +- .../courseware/instructor_dashboard.html | 141 ++++++++++++++++++ lms/urls.py | 1 + 4 files changed, 254 insertions(+), 36 deletions(-) diff --git a/lms/djangoapps/courseware/tasks.py b/lms/djangoapps/courseware/tasks.py index 516997485e..674ea1effc 100644 --- a/lms/djangoapps/courseware/tasks.py +++ b/lms/djangoapps/courseware/tasks.py @@ -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 diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 47b22edcb2..67ea0d1ea9 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -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 += 'Failed to create a background task for regrading "{0}".'.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 += "
Grades from %s" % 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), diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index f60f591f2f..3b5ea7a0e1 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -9,6 +9,110 @@ + +%if course_tasks is not None: + +%endif @@ -384,6 +488,43 @@ function goto( mode) %if msg:

${msg}

%endif +##----------------------------------------------------------------------------- +## Output tasks in progress + +%if course_tasks is not None: +

Pending Course Tasks

+
+ + + + + + + + + + + + + %for tasknum, course_task in enumerate(course_tasks): + + + + + + + + + + + + %endfor +
Task NameTask ArgStudentTask IdRequesterSubmittedLast UpdateTask StatusTask Progress
${course_task.task_name}${course_task.task_args}${course_task.student}
${course_task.task_id}
${course_task.requester}${course_task.created}
${course_task.updated}
${course_task.task_status}
unknown
+
+
+ +%endif + ##----------------------------------------------------------------------------- %if modeflag.get('Analytics'): diff --git a/lms/urls.py b/lms/urls.py index 74ac44cf59..60d84d4e74 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -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