Files
edx-platform/lms/djangoapps/courseware/tasks.py
Brian Wilson 95c1c4b8a8 Refactor tracking and xqueue initialization to no longer require a request. Get tracking logs to work. Add transaction annotations.
Make sure to read only once from result objects when getting result status,
so that values are consistent.
2013-06-18 11:04:05 -04:00

302 lines
13 KiB
Python

import json
from time import sleep
from django.contrib.auth.models import User
from django.db import transaction
from celery import task, current_task
# from celery.signals import worker_ready
from celery.utils.log import get_task_logger
import mitxmako.middleware as middleware
from courseware.models import StudentModule, CourseTaskLog
from courseware.model_data import ModelDataCache
# from courseware.module_render import get_module
from courseware.module_render import get_module_for_descriptor_internal
from xmodule.modulestore.django import modulestore
from track.views import task_track
# define different loggers for use within tasks and on client side
task_log = get_task_logger(__name__)
@task
def waitawhile(value):
for i in range(value):
sleep(1) # in seconds
task_log.info('Waited {0} seconds...'.format(i))
current_task.update_state(state='PROGRESS',
meta={'current': i, 'total': value})
result = 'Yeah!'
return result
class UpdateProblemModuleStateError(Exception):
pass
def _update_problem_module_state(course_id, module_state_key, student, update_fcn, action_name, filter_fcn,
xmodule_instance_args):
"""
Performs generic update by visiting StudentModule instances with the update_fcn provided.
If student is None, performs update on modules for all students on the specified problem.
"""
# add hack so that mako templates will work on celery worker server:
# The initialization of Make templating is usually done when Django is
# initializing 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:
task_log.info("Initializing Mako middleware explicitly")
middleware.MakoMiddleware()
# find the problem descriptor:
module_descriptor = modulestore().get_instance(course_id, module_state_key)
# find the module in question
modules_to_update = StudentModule.objects.filter(course_id=course_id,
module_state_key=module_state_key)
# give the option of regrading an individual student. If not specified,
# then regrades all students who have responded to a problem so far
if student is not None:
modules_to_update = modules_to_update.filter(student_id=student.id)
if filter_fcn is not None:
modules_to_update = filter_fcn(modules_to_update)
# perform the main loop
num_updated = 0
num_attempted = 0
num_total = len(modules_to_update) # TODO: make this more efficient. Count()?
def get_task_progress():
progress = {'action_name': action_name,
'attempted': num_attempted,
'updated': num_updated,
'total': num_total,
}
return progress
task_log.info("Starting to process task {0}".format(current_task.request.id))
for module_to_update in modules_to_update:
num_attempted += 1
# There is no try here: if there's an error, we let it throw, and the task will
# be marked as FAILED, with a stack trace.
if update_fcn(module_descriptor, module_to_update, xmodule_instance_args):
# If the update_fcn returns true, then it performed some kind of work.
num_updated += 1
# update task status:
# TODO: decide on the frequency for updating this:
# -- it may not make sense to do so every time through the loop
# -- may depend on each iteration's duration
current_task.update_state(state='PROGRESS', meta=get_task_progress())
# TODO: remove this once done with manual testing
sleep(5) # in seconds
task_progress = get_task_progress()
current_task.update_state(state='PROGRESS', meta=task_progress)
task_log.info("Finished processing task")
return task_progress
def _update_problem_module_state_for_student(course_id, problem_url, student_identifier,
update_fcn, action_name, filter_fcn=None, xmodule_instance_args=None):
msg = ''
success = False
# try to uniquely id student by email address or username
try:
if "@" in student_identifier:
student_to_update = User.objects.get(email=student_identifier)
elif student_identifier is not None:
student_to_update = User.objects.get(username=student_identifier)
return _update_problem_module_state(course_id, problem_url, student_to_update, update_fcn,
action_name, filter_fcn, xmodule_instance_args)
except User.DoesNotExist:
msg = "Couldn't find student with that email or username."
return (success, msg)
def _update_problem_module_state_for_all_students(course_id, problem_url, update_fcn, action_name, filter_fcn=None, xmodule_instance_args=None):
return _update_problem_module_state(course_id, problem_url, None, update_fcn, action_name, filter_fcn, xmodule_instance_args)
def _get_module_instance_for_task(course_id, student, module_descriptor, module_state_key, xmodule_instance_args=None,
grade_bucket_type=None):
# reconstitute the problem's corresponding XModule:
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, student, module_descriptor)
# Note that the request is passed to get_module() to provide xqueue-related URL information
# instance = get_module(student, request, module_state_key, model_data_cache,
# course_id, grade_bucket_type='regrade')
request_info = xmodule_instance_args.get('request_info', {}) if xmodule_instance_args is not None else {}
task_info = {}
def make_track_function():
'''
Make a tracking function that logs what happened.
For insertion into ModuleSystem, and use by CapaModule.
'''
def f(event_type, event):
return task_track(request_info, task_info, event_type, event, page='x_module_task')
return f
xqueue_callback_url_prefix = ''
if xmodule_instance_args is not None:
xqueue_callback_url_prefix = xmodule_instance_args.get('xqueue_callback_url_prefix')
return get_module_for_descriptor_internal(student, module_descriptor, model_data_cache, course_id,
make_track_function(), xqueue_callback_url_prefix,
grade_bucket_type=grade_bucket_type)
@transaction.autocommit
def _regrade_problem_module_state(module_descriptor, student_module, xmodule_instance_args=None):
'''
Takes an XModule descriptor and a corresponding StudentModule object, and
performs regrading on the student's problem submission.
Throws exceptions if the regrading is fatal and should be aborted if in a loop.
'''
# unpack the StudentModule:
course_id = student_module.course_id
student = student_module.student
module_state_key = student_module.module_state_key
instance = _get_module_instance_for_task(course_id, student, module_descriptor, module_state_key, xmodule_instance_args, grade_bucket_type='regrade')
if instance is None:
# Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to.
msg = "No module {loc} for student {student}--access denied?".format(loc=module_state_key,
student=student)
task_log.debug(msg)
raise UpdateProblemModuleStateError(msg)
if not hasattr(instance, 'regrade_problem'):
# if the first instance doesn't have a regrade method, we should
# probably assume that no other instances will either.
msg = "Specified problem does not support regrading."
raise UpdateProblemModuleStateError(msg)
result = instance.regrade_problem()
if 'success' not in result:
# don't consider these fatal, but false means that the individual call didn't complete:
task_log.warning("error processing regrade call for problem {loc} and student {student}: "
"unexpected response {msg}".format(msg=result, loc=module_state_key, student=student))
return False
elif result['success'] != 'correct' and result['success'] != 'incorrect':
task_log.warning("error processing regrade call for problem {loc} and student {student}: "
"{msg}".format(msg=result['success'], loc=module_state_key, student=student))
return False
else:
task_log.debug("successfully processed regrade call for problem {loc} and student {student}: "
"{msg}".format(msg=result['success'], loc=module_state_key, student=student))
return True
def filter_problem_module_state_for_done(modules_to_update):
return modules_to_update.filter(state__contains='"done": true')
@task
def regrade_problem_for_student(course_id, problem_url, student_identifier, xmodule_instance_args):
action_name = 'regraded'
update_fcn = _regrade_problem_module_state
filter_fcn = filter_problem_module_state_for_done
return _update_problem_module_state_for_student(course_id, problem_url, student_identifier,
update_fcn, action_name, filter_fcn, xmodule_instance_args)
@task
def regrade_problem_for_all_students(course_id, problem_url, xmodule_instance_args):
# factory = RequestFactory(**request_environ)
# request = factory.get('/')
action_name = 'regraded'
update_fcn = _regrade_problem_module_state
filter_fcn = filter_problem_module_state_for_done
return _update_problem_module_state_for_all_students(course_id, problem_url, update_fcn, action_name, filter_fcn,
xmodule_instance_args)
@transaction.autocommit
def _reset_problem_attempts_module_state(module_descriptor, student_module, xmodule_instance_args=None):
# modify the problem's state
# load the state json and change state
problem_state = json.loads(student_module.state)
if 'attempts' in problem_state:
old_number_of_attempts = problem_state["attempts"]
if old_number_of_attempts > 0:
problem_state["attempts"] = 0
# convert back to json and save
student_module.state = json.dumps(problem_state)
student_module.save()
# consider the reset to be successful, even if no update was performed. (It's just "optimized".)
return True
@task
def reset_problem_attempts_for_student(course_id, problem_url, student_identifier):
action_name = 'reset'
update_fcn = _reset_problem_attempts_module_state
return _update_problem_module_state_for_student(course_id, problem_url, student_identifier,
update_fcn, action_name)
@task
def reset_problem_attempts_for_all_students(course_id, problem_url):
action_name = 'reset'
update_fcn = _reset_problem_attempts_module_state
return _update_problem_module_state_for_all_students(course_id, problem_url,
update_fcn, action_name)
@transaction.autocommit
def _delete_problem_module_state(module_descriptor, student_module, xmodule_instance_args=None):
"""Delete the StudentModule entry."""
student_module.delete()
return True
@task
def delete_problem_state_for_student(course_id, problem_url, student_ident):
action_name = 'deleted'
update_fcn = _delete_problem_module_state
return _update_problem_module_state_for_student(course_id, problem_url, student_ident,
update_fcn, action_name)
@task
def delete_problem_state_for_all_students(course_id, problem_url):
action_name = 'deleted'
update_fcn = _delete_problem_module_state
return _update_problem_module_state_for_all_students(course_id, problem_url,
update_fcn, action_name)
#@worker_ready.connect
#def initialize_middleware(**kwargs):
# # The initialize Django middleware - some middleware components
# # are initialized lazily when the first request is served. Since
# # the celery workers do not serve request, the components never
# # get initialized, causing errors in some dependencies.
# # In particular, the Mako template middleware is used by some xmodules
# task_log.info("Initializing all middleware from worker_ready.connect hook")
#
# from django.core.handlers.base import BaseHandler
# BaseHandler().load_middleware()