Commit 0d38789a by Brian Wilson

Add additional background tasks: reset attempts, delete state. Update

CourseTaskLog fully after task submission, so it works in Eager mode
(for testing).
parent 95c1c4b8
...@@ -8,7 +8,8 @@ from celery.states import READY_STATES ...@@ -8,7 +8,8 @@ from celery.states import READY_STATES
from courseware.models import CourseTaskLog from courseware.models import CourseTaskLog
from courseware.module_render import get_xqueue_callback_url_prefix from courseware.module_render import get_xqueue_callback_url_prefix
from courseware.tasks import regrade_problem_for_all_students from courseware.tasks import (regrade_problem_for_all_students, regrade_problem_for_student,
reset_problem_attempts_for_all_students, delete_problem_state_for_all_students)
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -16,13 +17,53 @@ log = logging.getLogger(__name__) ...@@ -16,13 +17,53 @@ log = logging.getLogger(__name__)
def get_running_course_tasks(course_id): def get_running_course_tasks(course_id):
"""Returns a query of CourseTaskLog objects of running tasks for a given course.""" """
Returns a query of CourseTaskLog objects of running tasks for a given course.
Used to generate a list of tasks to display on the instructor dashboard.
"""
course_tasks = CourseTaskLog.objects.filter(course_id=course_id) course_tasks = CourseTaskLog.objects.filter(course_id=course_id)
for state in READY_STATES: for state in READY_STATES:
course_tasks = course_tasks.exclude(task_state=state) course_tasks = course_tasks.exclude(task_state=state)
return course_tasks return course_tasks
def course_task_log_status(request, task_id=None):
"""
This returns the status of a course-related task as a JSON-serialized dict.
The task_id can be specified in one of three ways:
* explicitly as an argument to the method (by specifying in the url)
Returns a dict containing status information for the specified task_id
* by making a post request containing 'task_id' as a parameter with a single value
Returns a dict containing status information for the specified task_id
* by making a post request containing 'task_ids' as a parameter,
with a list of task_id values.
Returns a dict of dicts, with the task_id as key, and the corresponding
dict containing status information for the specified task_id
Task_id values that are unrecognized are skipped.
"""
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)
if task_output is not None:
output[task_id] = task_output
return HttpResponse(json.dumps(output, indent=4))
def _task_is_running(course_id, task_name, task_args, student=None): def _task_is_running(course_id, task_name, task_args, student=None):
"""Checks if a particular task is already running""" """Checks if a particular task is already running"""
runningTasks = CourseTaskLog.objects.filter(course_id=course_id, task_name=task_name, task_args=task_args) runningTasks = CourseTaskLog.objects.filter(course_id=course_id, task_name=task_name, task_args=task_args)
...@@ -66,9 +107,7 @@ def _update_task(course_task_log, task_result): ...@@ -66,9 +107,7 @@ def _update_task(course_task_log, task_result):
Autocommit annotation makes sure the database entry is committed. Autocommit annotation makes sure the database entry is committed.
""" """
course_task_log.task_state = task_result.state _update_course_task_log(course_task_log, task_result)
course_task_log.task_id = task_result.id
course_task_log.save()
def _get_xmodule_instance_args(request): def _get_xmodule_instance_args(request):
...@@ -91,42 +130,68 @@ def _get_xmodule_instance_args(request): ...@@ -91,42 +130,68 @@ def _get_xmodule_instance_args(request):
return xmodule_instance_args return xmodule_instance_args
def course_task_log_status(request, task_id=None): def _update_course_task_log(course_task_log_entry, task_result):
""" """
This returns the status of a course-related task as a JSON-serialized dict. Updates and possibly saves a CourseTaskLog entry based on a task Result.
The task_id can be specified in one of three ways: Used when a task initially returns, as well as when updated status is
requested.
* explicitly as an argument to the method (by specifying in the url) Calculates json to store in task_progress field.
Returns a dict containing status information for the specified task_id """
task_id = task_result.task_id
result_state = task_result.state
returned_result = task_result.result
result_traceback = task_result.traceback
* by making a post request containing 'task_id' as a parameter with a single value # Assume we don't always update the CourseTaskLog entry if we don't have to:
Returns a dict containing status information for the specified task_id entry_needs_saving = False
output = {}
* by making a post request containing 'task_ids' as a parameter, if result_state == 'PROGRESS':
with a list of task_id values. # construct a status message directly from the task result's result:
Returns a dict of dicts, with the task_id as key, and the corresponding if hasattr(task_result, 'result') and 'attempted' in returned_result:
dict containing status information for the specified task_id fmt = "Attempted {attempted} of {total}, {action_name} {updated}"
message = fmt.format(attempted=returned_result['attempted'],
updated=returned_result['updated'],
total=returned_result['total'],
action_name=returned_result['action_name'])
output['message'] = message
log.info("task progress: {0}".format(message))
else:
log.info("still making progress... ")
output['task_progress'] = returned_result
Task_id values that are unrecognized are skipped. elif result_state == 'SUCCESS':
output['task_progress'] = returned_result
course_task_log_entry.task_progress = json.dumps(returned_result)
log.info("task succeeded: {0}".format(returned_result))
entry_needs_saving = True
""" elif result_state == 'FAILURE':
output = {} # on failure, the result's result contains the exception that caused the failure
if task_id is not None: exception = returned_result
output = _get_course_task_log_status(task_id) traceback = result_traceback if result_traceback is not None else ''
elif 'task_id' in request.POST: entry_needs_saving = True
task_id = request.POST['task_id'] task_progress = {'exception': type(exception).__name__, 'message': str(exception.message)}
output = _get_course_task_log_status(task_id) output['message'] = exception.message
elif 'task_ids[]' in request.POST: log.warning("background task (%s) failed: %s %s".format(task_id, returned_result, traceback))
tasks = request.POST.getlist('task_ids[]') if result_traceback is not None:
for task_id in tasks: output['task_traceback'] = result_traceback
task_output = _get_course_task_log_status(task_id) task_progress['traceback'] = result_traceback
if task_output is not None: course_task_log_entry.task_progress = json.dumps(task_progress)
output[task_id] = task_output output['task_progress'] = task_progress
# TODO decide whether to raise exception if bad args are passed.
# May be enough just to return an empty output.
return HttpResponse(json.dumps(output, indent=4)) # always update the entry if the state has changed:
if result_state != course_task_log_entry.task_state:
course_task_log_entry.task_state = result_state
course_task_log_entry.task_id = task_id
entry_needs_saving = True
if entry_needs_saving:
course_task_log_entry.save()
return output
def _get_course_task_log_status(task_id): def _get_course_task_log_status(task_id):
...@@ -169,56 +234,12 @@ def _get_course_task_log_status(task_id): ...@@ -169,56 +234,12 @@ def _get_course_task_log_status(task_id):
# Just create the result object, and pull values out once. # Just create the result object, and pull values out once.
# (If we check them later, the state and result may have changed.) # (If we check them later, the state and result may have changed.)
result = AsyncResult(task_id) result = AsyncResult(task_id)
result_state = result.state output.update(_update_course_task_log(course_task_log_entry, result))
returned_result = result.result elif course_task_log_entry.task_progress is not None:
result_traceback = result.traceback
# Assume we don't always update the CourseTaskLog entry if we don't have to:
entry_needs_saving = False
if result_state == 'PROGRESS':
# construct a status message directly from the task result's result:
if hasattr(result, 'result') and 'attempted' in returned_result:
fmt = "Attempted {attempted} of {total}, {action_name} {updated}"
message = fmt.format(attempted=returned_result['attempted'],
updated=returned_result['updated'],
total=returned_result['total'],
action_name=returned_result['action_name'])
output['message'] = message
log.info("task progress: {0}".format(message))
else:
log.info("still making progress... ")
output['task_progress'] = returned_result
elif result_state == 'SUCCESS':
# on success, save out the result here, but the message
# will be calculated later
output['task_progress'] = returned_result
course_task_log_entry.task_progress = json.dumps(returned_result)
log.info("task succeeded: {0}".format(returned_result))
entry_needs_saving = True
elif result_state == 'FAILURE':
# on failure, the result's result contains the exception that caused the failure
exception = str(returned_result)
course_task_log_entry.task_progress = exception
entry_needs_saving = True
output['message'] = exception
log.info("task failed: {0}".format(returned_result))
if result_traceback is not None:
output['task_traceback'] = result_traceback
# always update the entry if the state has changed:
if result_state != course_task_log_entry.task_state:
course_task_log_entry.task_state = result_state
entry_needs_saving = True
if entry_needs_saving:
course_task_log_entry.save()
else:
# task is already known to have finished, but report on its status: # task is already known to have finished, but report on its status:
if course_task_log_entry.task_progress is not None:
output['task_progress'] = json.loads(course_task_log_entry.task_progress) output['task_progress'] = json.loads(course_task_log_entry.task_progress)
if course_task_log_entry.task_state == 'FAILURE':
output['message'] = output['task_progress']['message']
# output basic information matching what's stored in CourseTaskLog: # output basic information matching what's stored in CourseTaskLog:
output['task_id'] = course_task_log_entry.task_id output['task_id'] = course_task_log_entry.task_id
...@@ -274,14 +295,48 @@ def _get_task_completion_message(course_task_log_entry): ...@@ -274,14 +295,48 @@ def _get_task_completion_message(course_task_log_entry):
return (succeeded, message) return (succeeded, message)
########### Add task-submission methods here:
def submit_regrade_problem_for_student(request, course_id, problem_url, student):
"""
Request a problem to be regraded as a background task.
The problem will be regraded for the specified student only. Parameters are the `course_id`,
the `problem_url`, and the `student` as a User object.
The url must specify the location of the problem, using i4x-type notation.
An exception is thrown if the problem doesn't exist, or if the particular
problem is already being regraded for this student.
"""
# check arguments: make sure that the problem_url is defined
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
# an exception will be raised. Let it pass up to the caller.
modulestore().get_instance(course_id, problem_url)
task_name = 'regrade_problem'
# check to see if task is already running, and reserve it otherwise
course_task_log = _reserve_task(course_id, task_name, problem_url, request.user, student)
# Submit task:
task_args = [course_id, problem_url, student.username, _get_xmodule_instance_args(request)]
task_result = regrade_problem_for_student.apply_async(task_args)
# Update info in table with the resulting task_id (and state).
_update_task(course_task_log, task_result)
return course_task_log
def submit_regrade_problem_for_all_students(request, course_id, problem_url): def submit_regrade_problem_for_all_students(request, course_id, problem_url):
""" """
Request a problem to be regraded as a background task. Request a problem to be regraded as a background task.
The problem will be regraded for all students who have accessed the The problem will be regraded for all students who have accessed the
particular problem in a course. Parameters are the `course_id` and particular problem in a course and have provided and checked an answer.
the `problem_url`. The url must specify the location of the problem, Parameters are the `course_id` and the `problem_url`.
using i4x-type notation. The url must specify the location of the problem, using i4x-type notation.
An exception is thrown if the problem doesn't exist, or if the particular An exception is thrown if the problem doesn't exist, or if the particular
problem is already being regraded. problem is already being regraded.
...@@ -304,3 +359,67 @@ def submit_regrade_problem_for_all_students(request, course_id, problem_url): ...@@ -304,3 +359,67 @@ def submit_regrade_problem_for_all_students(request, course_id, problem_url):
_update_task(course_task_log, task_result) _update_task(course_task_log, task_result)
return course_task_log return course_task_log
def submit_reset_problem_attempts_for_all_students(request, course_id, problem_url):
"""
Request to have attempts reset for a problem as a background task.
The problem's attempts will be reset for all students who have accessed the
particular problem in a course. Parameters are the `course_id` and
the `problem_url`. The url must specify the location of the problem,
using i4x-type notation.
An exception is thrown if the problem doesn't exist, or if the particular
problem is already being reset.
"""
# check arguments: make sure that the problem_url is defined
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
# an exception will be raised. Let it pass up to the caller.
modulestore().get_instance(course_id, problem_url)
task_name = 'reset_problem_attempts'
# check to see if task is already running, and reserve it otherwise
course_task_log = _reserve_task(course_id, task_name, problem_url, request.user)
# Submit task:
task_args = [course_id, problem_url, _get_xmodule_instance_args(request)]
task_result = reset_problem_attempts_for_all_students.apply_async(task_args)
# Update info in table with the resulting task_id (and state).
_update_task(course_task_log, task_result)
return course_task_log
def submit_delete_problem_state_for_all_students(request, course_id, problem_url):
"""
Request to have state deleted for a problem as a background task.
The problem's state will be deleted for all students who have accessed the
particular problem in a course. Parameters are the `course_id` and
the `problem_url`. The url must specify the location of the problem,
using i4x-type notation.
An exception is thrown if the problem doesn't exist, or if the particular
problem is already being deleted.
"""
# check arguments: make sure that the problem_url is defined
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
# an exception will be raised. Let it pass up to the caller.
modulestore().get_instance(course_id, problem_url)
task_name = 'delete_problem_state'
# check to see if task is already running, and reserve it otherwise
course_task_log = _reserve_task(course_id, task_name, problem_url, request.user)
# Submit task:
task_args = [course_id, problem_url, _get_xmodule_instance_args(request)]
task_result = delete_problem_state_for_all_students.apply_async(task_args)
# Update info in table with the resulting task_id (and state).
_update_task(course_task_log, task_result)
return course_task_log
...@@ -11,7 +11,7 @@ from celery.utils.log import get_task_logger ...@@ -11,7 +11,7 @@ from celery.utils.log import get_task_logger
import mitxmako.middleware as middleware import mitxmako.middleware as middleware
from courseware.models import StudentModule, CourseTaskLog from courseware.models import StudentModule
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 courseware.module_render import get_module_for_descriptor_internal from courseware.module_render import get_module_for_descriptor_internal
...@@ -25,18 +25,6 @@ from track.views import task_track ...@@ -25,18 +25,6 @@ from track.views import task_track
task_log = get_task_logger(__name__) 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): class UpdateProblemModuleStateError(Exception):
pass pass
...@@ -48,6 +36,13 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc ...@@ -48,6 +36,13 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc
If student is None, performs update on modules for all students on the specified problem. If student is None, performs update on modules for all students on the specified problem.
""" """
task_id = current_task.request.id
fmt = 'Starting to update problem modules as task "{task_id}": course "{course_id}" problem "{state_key}": nothing {action} yet'
task_log.info(fmt.format(task_id=task_id, course_id=course_id, state_key=module_state_key, action=action_name))
# add task_id to xmodule_instance_args, so that it can be output with tracking info:
xmodule_instance_args['task_id'] = task_id
# add hack so that mako templates will work on celery worker server: # add hack so that mako templates will work on celery worker server:
# The initialization of Make templating is usually done when Django is # The initialization of Make templating is usually done when Django is
# initializing middleware packages as part of processing a server request. # initializing middleware packages as part of processing a server request.
...@@ -86,7 +81,6 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc ...@@ -86,7 +81,6 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc
} }
return progress return progress
task_log.info("Starting to process task {0}".format(current_task.request.id))
for module_to_update in modules_to_update: for module_to_update in modules_to_update:
num_attempted += 1 num_attempted += 1
...@@ -142,8 +136,10 @@ def _get_module_instance_for_task(course_id, student, module_descriptor, module_ ...@@ -142,8 +136,10 @@ def _get_module_instance_for_task(course_id, student, module_descriptor, module_
# instance = get_module(student, request, module_state_key, model_data_cache, # instance = get_module(student, request, module_state_key, model_data_cache,
# course_id, grade_bucket_type='regrade') # course_id, grade_bucket_type='regrade')
# get request-related tracking information from args passthrough, and supplement with task-specific
# information:
request_info = xmodule_instance_args.get('request_info', {}) if xmodule_instance_args is not None else {} request_info = xmodule_instance_args.get('request_info', {}) if xmodule_instance_args is not None else {}
task_info = {} task_info = {"student": student.username, "task_id": xmodule_instance_args['task_id']}
def make_track_function(): def make_track_function():
''' '''
...@@ -250,19 +246,21 @@ def _reset_problem_attempts_module_state(module_descriptor, student_module, xmod ...@@ -250,19 +246,21 @@ def _reset_problem_attempts_module_state(module_descriptor, student_module, xmod
@task @task
def reset_problem_attempts_for_student(course_id, problem_url, student_identifier): def reset_problem_attempts_for_student(course_id, problem_url, student_identifier, xmodule_instance_args):
action_name = 'reset' action_name = 'reset'
update_fcn = _reset_problem_attempts_module_state update_fcn = _reset_problem_attempts_module_state
return _update_problem_module_state_for_student(course_id, problem_url, student_identifier, return _update_problem_module_state_for_student(course_id, problem_url, student_identifier,
update_fcn, action_name) update_fcn, action_name,
xmodule_instance_args=xmodule_instance_args)
@task @task
def reset_problem_attempts_for_all_students(course_id, problem_url): def reset_problem_attempts_for_all_students(course_id, problem_url, xmodule_instance_args):
action_name = 'reset' action_name = 'reset'
update_fcn = _reset_problem_attempts_module_state update_fcn = _reset_problem_attempts_module_state
return _update_problem_module_state_for_all_students(course_id, problem_url, return _update_problem_module_state_for_all_students(course_id, problem_url,
update_fcn, action_name) update_fcn, action_name,
xmodule_instance_args=xmodule_instance_args)
@transaction.autocommit @transaction.autocommit
...@@ -273,19 +271,21 @@ def _delete_problem_module_state(module_descriptor, student_module, xmodule_inst ...@@ -273,19 +271,21 @@ def _delete_problem_module_state(module_descriptor, student_module, xmodule_inst
@task @task
def delete_problem_state_for_student(course_id, problem_url, student_ident): def delete_problem_state_for_student(course_id, problem_url, student_ident, xmodule_instance_args):
action_name = 'deleted' action_name = 'deleted'
update_fcn = _delete_problem_module_state update_fcn = _delete_problem_module_state
return _update_problem_module_state_for_student(course_id, problem_url, student_ident, return _update_problem_module_state_for_student(course_id, problem_url, student_ident,
update_fcn, action_name) update_fcn, action_name,
xmodule_instance_args=xmodule_instance_args)
@task @task
def delete_problem_state_for_all_students(course_id, problem_url): def delete_problem_state_for_all_students(course_id, problem_url, xmodule_instance_args):
action_name = 'deleted' action_name = 'deleted'
update_fcn = _delete_problem_module_state update_fcn = _delete_problem_module_state
return _update_problem_module_state_for_all_students(course_id, problem_url, return _update_problem_module_state_for_all_students(course_id, problem_url,
update_fcn, action_name) update_fcn, action_name,
xmodule_instance_args=xmodule_instance_args)
#@worker_ready.connect #@worker_ready.connect
......
...@@ -10,9 +10,7 @@ import os ...@@ -10,9 +10,7 @@ import os
import re import re
import requests import requests
from requests.status_codes import codes from requests.status_codes import codes
#import urllib
from collections import OrderedDict from collections import OrderedDict
#from time import sleep
from StringIO import StringIO from StringIO import StringIO
...@@ -25,7 +23,6 @@ from mitxmako.shortcuts import render_to_response ...@@ -25,7 +23,6 @@ from mitxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware import grades from courseware import grades
#from courseware import tasks # for now... should remove once things are in queue instead
from courseware import task_queue from courseware import task_queue
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)
...@@ -43,6 +40,7 @@ import xmodule.graders as xmgraders ...@@ -43,6 +40,7 @@ import xmodule.graders as xmgraders
import track.views import track.views
from .offline_gradecalc import student_grades, offline_grades_available from .offline_gradecalc import student_grades, offline_grades_available
from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -139,6 +137,25 @@ def instructor_dashboard(request, course_id): ...@@ -139,6 +137,25 @@ def instructor_dashboard(request, course_id):
(group, _) = Group.objects.get_or_create(name=name) (group, _) = Group.objects.get_or_create(name=name)
return group return group
def get_module_url(urlname):
"""
Construct full URL for a module from its urlname.
Form is either urlname or modulename/urlname. If no modulename
is provided, "problem" is assumed.
"""
# tolerate an XML suffix in the urlname
if urlname[-4:] == ".xml":
urlname = urlname[:-4]
# implement default
if '/' not in urlname:
urlname = "problem/" + urlname
# complete the url using information about the current course:
(org, course_name, _) = course_id.split("/")
return "i4x://" + org + "/" + course_name + "/" + urlname
# process actions from form POST # process actions from form POST
action = request.POST.get('action', '') action = request.POST.get('action', '')
use_offline = request.POST.get('use_offline_grades', False) use_offline = request.POST.get('use_offline_grades', False)
...@@ -177,13 +194,6 @@ def instructor_dashboard(request, course_id): ...@@ -177,13 +194,6 @@ def instructor_dashboard(request, course_id):
datatable['title'] = 'List of students enrolled in {0}'.format(course_id) datatable['title'] = 'List of students enrolled in {0}'.format(course_id)
track.views.server_track(request, 'list-students', {}, page='idashboard') track.views.server_track(request, 'list-students', {}, page='idashboard')
# elif 'Test Celery' in action:
# args = (10,)
# result = tasks.waitawhile.apply_async(args, retry=False)
# task_id = result.id
# celery_ajax_url = reverse('celery_ajax_status', kwargs={'task_id': task_id})
# msg += '<p>Celery Status for task ${task}:</p><div class="celery-status" data-ajax_url="${url}"></div><p>Status end.</p>'.format(task=task_id, url=celery_ajax_url)
elif 'Dump Grades' in action: elif 'Dump Grades' in action:
log.debug(action) log.debug(action)
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline)
...@@ -216,7 +226,8 @@ def instructor_dashboard(request, course_id): ...@@ -216,7 +226,8 @@ def instructor_dashboard(request, course_id):
msg += dump_grading_context(course) msg += dump_grading_context(course)
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_urlname = request.POST.get('problem_for_all_students', '')
problem_url = get_module_url(problem_urlname)
try: try:
course_task_log_entry = task_queue.submit_regrade_problem_for_all_students(request, course_id, problem_url) course_task_log_entry = task_queue.submit_regrade_problem_for_all_students(request, course_id, problem_url)
if course_task_log_entry is None: if course_task_log_entry is None:
...@@ -224,66 +235,103 @@ def instructor_dashboard(request, course_id): ...@@ -224,66 +235,103 @@ def instructor_dashboard(request, course_id):
else: else:
track_msg = 'regrade problem {problem} for all students in {course}'.format(problem=problem_url, course=course_id) track_msg = 'regrade problem {problem} for all students in {course}'.format(problem=problem_url, course=course_id)
track.views.server_track(request, track_msg, {}, page='idashboard') track.views.server_track(request, track_msg, {}, page='idashboard')
except ItemNotFoundError as e:
log.error('Failure to regrade: unknown problem "{0}"'.format(e))
msg += '<font color="red">Failed to create a background task for regrading "{0}": problem not found.</font>'.format(problem_url)
except Exception as e: except Exception as e:
log.error("Encountered exception from regrade: {0}".format(e)) log.error("Encountered exception from regrade: {0}".format(e))
msg += '<font="red">Failed to create a background task for regrading "{0}": {1}.</font>'.format(problem_url, e) msg += '<font color="red">Failed to create a background task for regrading "{0}": {1}.</font>'.format(problem_url, e.message)
elif "Reset ALL students' attempts" in action:
problem_urlname = request.POST.get('problem_for_all_students', '')
problem_url = get_module_url(problem_urlname)
try:
course_task_log_entry = task_queue.submit_reset_problem_attempts_for_all_students(request, course_id, problem_url)
if course_task_log_entry is None:
msg += '<font color="red">Failed to create a background task for resetting "{0}".</font>'.format(problem_url)
else:
track_msg = 'reset problem {problem} for all students in {course}'.format(problem=problem_url, course=course_id)
track.views.server_track(request, track_msg, {}, page='idashboard')
except ItemNotFoundError as e:
log.error('Failure to reset: unknown problem "{0}"'.format(e))
msg += '<font color="red">Failed to create a background task for resetting "{0}": problem not found.</font>'.format(problem_url)
except Exception as e:
log.error("Encountered exception from reset: {0}".format(e))
msg += '<font color="red">Failed to create a background task for resetting "{0}": {1}.</font>'.format(problem_url, e.message)
elif "Delete ALL student state for module" in action:
problem_urlname = request.POST.get('problem_for_all_students', '')
problem_url = get_module_url(problem_urlname)
try:
course_task_log_entry = task_queue.submit_delete_problem_state_for_all_students(request, course_id, problem_url)
if course_task_log_entry is None:
msg += '<font color="red">Failed to create a background task for deleting "{0}".</font>'.format(problem_url)
else:
track_msg = 'delete state for problem {problem} for all students in {course}'.format(problem=problem_url, course=course_id)
track.views.server_track(request, track_msg, {}, page='idashboard')
except ItemNotFoundError as e:
log.error('Failure to delete state: unknown problem "{0}"'.format(e))
msg += '<font color="red">Failed to create a background task for deleting state for "{0}": problem not found.</font>'.format(problem_url)
except Exception as e:
log.error("Encountered exception from delete state: {0}".format(e))
msg += '<font color="red">Failed to create a background task for deleting state for "{0}": {1}.</font>'.format(problem_url, e.message)
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 module" in action \
or "Regrade student's problem submission" 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', '')
problem_to_reset = request.POST.get('problem_to_reset', '') problem_urlname = request.POST.get('problem_for_student', '')
module_state_key = get_module_url(problem_urlname)
if problem_to_reset[-4:] == ".xml":
problem_to_reset = problem_to_reset[:-4]
# try to uniquely id student by email address or username # try to uniquely id student by email address or username
try: try:
if "@" in unique_student_identifier: if "@" in unique_student_identifier:
student_to_reset = User.objects.get(email=unique_student_identifier) student = User.objects.get(email=unique_student_identifier)
else: else:
student_to_reset = User.objects.get(username=unique_student_identifier) student = User.objects.get(username=unique_student_identifier)
msg += "Found a single student to reset. " msg += "Found a single student. "
except: except User.DoesNotExist:
student_to_reset = None student = None
msg += "<font color='red'>Couldn't find student with that email or username. </font>" msg += "<font color='red'>Couldn't find student with that email or username. </font>"
if student_to_reset is not None: student_module = None
if student is not None:
# find the module in question # find the module in question
if '/' not in problem_to_reset: # allow state of modules other than problem to be reset
problem_to_reset = "problem/" + problem_to_reset # but problem is the default
try: try:
(org, course_name, _) = course_id.split("/") student_module = StudentModule.objects.get(student_id=student.id,
module_state_key = "i4x://" + org + "/" + course_name + "/" + problem_to_reset
module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id,
course_id=course_id, course_id=course_id,
module_state_key=module_state_key) module_state_key=module_state_key)
msg += "Found module to reset. " msg += "Found module. "
except Exception: except StudentModule.DoesNotExist:
msg += "<font color='red'>Couldn't find module with that urlname. </font>" msg += "<font color='red'>Couldn't find module with that urlname. </font>"
if "Delete student state for problem" in action: if student_module is not None:
if "Delete student state for module" in action:
# delete the state # delete the state
try: try:
module_to_reset.delete() student_module.delete()
msg += "<font color='red'>Deleted student module state for %s!</font>" % module_state_key msg += "<font color='red'>Deleted student module state for %s!</font>" % module_state_key
track_msg = 'delete student module state for problem {problem} for student {student} in {course}'
track_msg = track_msg.format(problem=problem_url, student=unique_student_identifier, course=course_id)
track.views.server_track(request, track_msg, {}, page='idashboard')
except: except:
msg += "Failed to delete module state for %s/%s" % (unique_student_identifier, problem_to_reset) msg += "Failed to delete module state for %s/%s" % (unique_student_identifier, problem_urlname)
else: elif "Reset student's attempts" in action:
# modify the problem's state # modify the problem's state
try: try:
# load the state json # load the state json
problem_state = json.loads(module_to_reset.state) problem_state = json.loads(student_module.state)
old_number_of_attempts = problem_state["attempts"] old_number_of_attempts = problem_state["attempts"]
problem_state["attempts"] = 0 problem_state["attempts"] = 0
# save # save
module_to_reset.state = json.dumps(problem_state) student_module.state = json.dumps(problem_state)
module_to_reset.save() student_module.save()
track.views.server_track(request, track.views.server_track(request,
'{instructor} reset attempts from {old_attempts} to 0 for {student} on problem {problem} in {course}'.format( '{instructor} reset attempts from {old_attempts} to 0 for {student} on problem {problem} in {course}'.format(
old_attempts=old_number_of_attempts, old_attempts=old_number_of_attempts,
student=student_to_reset, student=student,
problem=module_to_reset.module_state_key, problem=student_module.module_state_key,
instructor=request.user, instructor=request.user,
course=course_id), course=course_id),
{}, {},
...@@ -291,6 +339,17 @@ def instructor_dashboard(request, course_id): ...@@ -291,6 +339,17 @@ def instructor_dashboard(request, course_id):
msg += "<font color='green'>Module state successfully reset!</font>" msg += "<font color='green'>Module state successfully reset!</font>"
except: except:
msg += "<font color='red'>Couldn't reset module state. </font>" msg += "<font color='red'>Couldn't reset module state. </font>"
else:
try:
course_task_log_entry = task_queue.submit_regrade_problem_for_student(request, course_id, module_state_key, student)
if course_task_log_entry is None:
msg += '<font color="red">Failed to create a background task for regrading "{0}" for student {1}.</font>'.format(module_state_key, unique_student_identifier)
else:
track_msg = 'regrade problem {problem} for student {student} in {course}'.format(problem=module_state_key, student=unique_student_identifier, course=course_id)
track.views.server_track(request, track_msg, {}, page='idashboard')
except Exception as e:
log.error("Encountered exception from regrade: {0}".format(e))
msg += '<font color="red">Failed to create a background task for regrading "{0}": {1}.</font>'.format(module_state_key, e.message)
elif "Get link to student's progress page" in action: elif "Get link to student's progress page" in action:
unique_student_identifier = request.POST.get('unique_student_identifier', '') unique_student_identifier = request.POST.get('unique_student_identifier', '')
...@@ -308,7 +367,7 @@ def instructor_dashboard(request, course_id): ...@@ -308,7 +367,7 @@ def instructor_dashboard(request, course_id):
{}, {},
page='idashboard') page='idashboard')
msg += "<a href='{0}' target='_blank'> Progress page for username: {1} with email address: {2}</a>.".format(progress_url, student_to_reset.username, student_to_reset.email) msg += "<a href='{0}' target='_blank'> Progress page for username: {1} with email address: {2}</a>.".format(progress_url, student_to_reset.username, student_to_reset.email)
except: except User.DoesNotExist:
msg += "<font color='red'>Couldn't find student with that username. </font>" msg += "<font color='red'>Couldn't find student with that username. </font>"
#---------------------------------------- #----------------------------------------
......
...@@ -19,93 +19,94 @@ ...@@ -19,93 +19,94 @@
this.CourseTaskProgress = (function() { this.CourseTaskProgress = (function() {
// Hardcode the refresh interval to be every five seconds.
// TODO: allow the refresh interval to be set. (And if it is disabled,
// then don't set the timeout at all.)
var refresh_interval = 5000;
// Hardcode the initial delay, for the first refresh, to two seconds:
var initial_refresh_delay = 2000;
function CourseTaskProgress(element) { function CourseTaskProgress(element) {
this.poll = __bind(this.poll, this); this.update_progress = __bind(this.update_progress, this);
this.queueing = __bind(this.queueing, this); this.get_status = __bind(this.get_status, this);
this.element = element; this.element = element;
this.reinitialize(element);
// start the work here
this.queueing();
}
CourseTaskProgress.prototype.reinitialize = function(element) {
this.entries = $(element).find('.task-progress-entry') this.entries = $(element).find('.task-progress-entry')
};
CourseTaskProgress.prototype.$ = function(selector) {
return $(selector, this.element);
};
CourseTaskProgress.prototype.queueing = function() {
if (window.queuePollerID) { if (window.queuePollerID) {
window.clearTimeout(window.queuePollerID); window.clearTimeout(window.queuePollerID);
} }
return window.queuePollerID = window.setTimeout(this.poll, 1000); return window.queuePollerID = window.setTimeout(this.get_status, this.initial_refresh_delay);
}
CourseTaskProgress.prototype.$ = function(selector) {
return $(selector, this.element);
}; };
CourseTaskProgress.prototype.poll = function() { CourseTaskProgress.prototype.update_progress = function(response) {
var _this = this; var _this = this;
// clear the array of entries to poll this time // Response should be a dict with an entry for each requested task_id,
this.task_ids = []; // with a "task-state" and "in_progress" key and optionally a "message"
// then go through the entries, update each, // and a "task_progress.duration" key.
// 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; var something_in_progress = false;
for (name in response) { for (task_id in response) {
if (response.hasOwnProperty(name)) {
var task_id = name;
var task_dict = response[task_id]; var task_dict = response[task_id];
// this should be a dict of properties for this task_id
if (task_dict.in_progress === true) {
something_in_progress = true;
}
// find the corresponding entry, and update it: // find the corresponding entry, and update it:
entry = $(_this.element).find('[data-task-id="' + task_id + '"]'); entry = $(_this.element).find('[data-task-id="' + task_id + '"]');
entry.find('.task-state').text(task_dict.task_state) entry.find('.task-state').text(task_dict.task_state)
var duration_value = (task_dict.task_progress && task_dict.task_progress.duration_ms) || 'unknown';
entry.find('.task-duration').text(duration_value);
var progress_value = task_dict.message || ''; var progress_value = task_dict.message || '';
entry.find('.task-progress').text(progress_value); entry.find('.task-progress').text(progress_value);
// if the task is complete, then change the entry so it won't
// be queried again. Otherwise set a flag.
if (task_dict.in_progress === true) {
something_in_progress = true;
} else {
entry.data('inProgress', "False")
} }
} }
// if some entries are still incomplete, then repoll:
if (something_in_progress) { if (something_in_progress) {
// TODO: set the refresh interval. (And if it is disabled, return window.queuePollerID = window.setTimeout(_this.get_status, _this.refresh_interval);
// then don't set the timeout at all.)
return window.queuePollerID = window.setTimeout(_this.poll, 1000);
} else { } else {
delete window.queuePollerID; delete window.queuePollerID;
} }
}
CourseTaskProgress.prototype.get_status = function() {
var _this = this;
var task_ids = [];
// Construct the array of ids to get status for, by
// including the subset of entries that are still in progress.
this.entries.each(function(idx, element) {
var task_id = $(element).data('taskId');
var in_progress = $(element).data('inProgress');
if (in_progress="True") {
task_ids.push(task_id);
}
}); });
};
// Make call to get status for these ids.
// Note that the keyname here ends up with "[]" being appended
// in the POST parameter that shows up on the Django server.
// TODO: add error handler.
var ajax_url = '/course_task_log_status/';
var data = {'task_ids': task_ids };
$.post(ajax_url, data).done(this.update_progress);
};
return CourseTaskProgress; return CourseTaskProgress;
})(); })();
}).call(this); }).call(this);
// once the page is rendered, create the progress object // once the page is rendered, create the progress object
var courseTaskProgress; var courseTaskProgress;
$(document).ready(function() { $(document).ready(function() {
courseTaskProgress = new CourseTaskProgress($('#task-progress-wrapper')); courseTaskProgress = new CourseTaskProgress($('#task-progress-wrapper'));
}); });
</script> </script>
%endif %endif
...@@ -294,25 +295,77 @@ function goto( mode) ...@@ -294,25 +295,77 @@ function goto( mode)
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
%endif %endif
%if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'):
<H2>Course-specific grade adjustment</h2> <H2>Course-specific grade adjustment</h2>
<p>to regrade a problem for all students, input the urlname of that problem</p> <p>
<p><input type="text" name="problem_to_regrade" size="60"> Specify a particular problem in the course here by its url:
<input type="text" name="problem_for_all_students" size="60">
</p>
<p>
You may use just the "urlname" if a problem, or "modulename/urlname" if not.
(For example, if the location is <tt>i4x://university/course/problem/problemname</tt>,
then just provide the <tt>problemname</tt>.
If the location is <tt>i4x://university/course/notaproblem/someothername</tt>, then
provide <tt>notaproblem/someothername</tt>.)
</p>
<p>
Then select an action:
<input type="submit" name="action" value="Reset ALL students' attempts">
<input type="submit" name="action" value="Regrade ALL students' problem submissions"> <input type="submit" name="action" value="Regrade ALL students' problem submissions">
</p> </p>
<p>
<p>These actions run in the background, and status for active tasks will appear in a table below.
To see status for all tasks submitted for this course, click on this button:
</p>
<p>
<input type="submit" name="action" value="Show Background Task History">
</p>
<hr width="40%" style="align:left">
%endif
<H2>Student-specific grade inspection and adjustment</h2> <H2>Student-specific grade inspection and adjustment</h2>
<p>edX email address or their username: </p> <p>
<p><input type="text" name="unique_student_identifier"> <input type="submit" name="action" value="Get link to student's progress page"></p> Specify the edX email address or username of a student here:
<p>and, if you want to reset the number of attempts for a problem, the urlname of that problem <input type="text" name="unique_student_identifier">
(e.g. if the location is <tt>i4x://university/course/problem/problemname</tt>, then the urlname is <tt>problemname</tt>).</p> </p>
<p> <input type="text" name="problem_to_reset" size="60"> <input type="submit" name="action" value="Reset student's attempts"> </p> <p>
Click this, and a link to student's progress page will appear below:
<input type="submit" name="action" value="Get link to student's progress page">
</p>
<p>
Specify a particular problem in the course here by its url:
<input type="text" name="problem_for_student" size="60">
</p>
<p>
You may use just the "urlname" if a problem, or "modulename/urlname" if not.
(For example, if the location is <tt>i4x://university/course/problem/problemname</tt>,
then just provide the <tt>problemname</tt>.
If the location is <tt>i4x://university/course/notaproblem/someothername</tt>, then
provide <tt>notaproblem/someothername</tt>.)
</p>
<p>
Then select an action:
<input type="submit" name="action" value="Reset student's attempts">
%if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'):
<input type="submit" name="action" value="Regrade student's problem submission">
%endif
</p>
%if instructor_access: %if instructor_access:
<p> You may also delete the entire state of a student for a problem: <p>
<input type="submit" name="action" value="Delete student state for problem"> </p> You may also delete the entire state of a student for the specified module:
<p>To delete the state of other XBlocks specify modulename/urlname, eg <input type="submit" name="action" value="Delete student state for module">
<tt>combinedopenended/Humanities_SA_Peer</tt></p> </p>
%endif
%if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'):
<p>Regrading runs in the background, and status for active tasks will appear in a table below.
To see status for all tasks submitted for this course and student, click on this button:
</p>
<p>
<input type="submit" name="action" value="Show Background Task History for Student">
</p>
%endif %endif
%endif %endif
...@@ -484,42 +537,6 @@ function goto( mode) ...@@ -484,42 +537,6 @@ function goto( mode)
%if msg: %if msg:
<p></p><p>${msg}</p> <p></p><p>${msg}</p>
%endif %endif
##-----------------------------------------------------------------------------
## Output tasks in progress
%if course_tasks is not None and len(course_tasks) > 0:
<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 State</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-state">${course_task.task_state}</div></td>
<td><div class="task-progress">unknown</div></td>
</tr>
%endfor
</table>
</div>
<br/>
%endif
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
...@@ -683,6 +700,47 @@ function goto( mode) ...@@ -683,6 +700,47 @@ function goto( mode)
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
## Output tasks in progress
%if course_tasks is not None and len(course_tasks) > 0:
<hr width="100%">
<h2>Pending Course Tasks</h2>
<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>Task State</th>
<th>Duration (ms)</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}"
data-in-progress="true">
<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-state">${course_task.task_state}</div></td>
<td><div class="task-duration">unknown</div></td>
<td><div class="task-progress">unknown</div></td>
</tr>
%endfor
</table>
</div>
<br/>
%endif
##-----------------------------------------------------------------------------
%if datatable and modeflag.get('Psychometrics') is None: %if datatable and modeflag.get('Psychometrics') is None:
<br/> <br/>
......
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