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
from courseware.models import CourseTaskLog
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
......@@ -16,13 +17,53 @@ log = logging.getLogger(__name__)
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)
for state in READY_STATES:
course_tasks = course_tasks.exclude(task_state=state)
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):
"""Checks if a particular task is already running"""
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):
Autocommit annotation makes sure the database entry is committed.
"""
course_task_log.task_state = task_result.state
course_task_log.task_id = task_result.id
course_task_log.save()
_update_course_task_log(course_task_log, task_result)
def _get_xmodule_instance_args(request):
......@@ -91,42 +130,68 @@ def _get_xmodule_instance_args(request):
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.
The task_id can be specified in one of three ways:
Updates and possibly saves a CourseTaskLog entry based on a task Result.
* 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.
Used when a task initially returns, as well as when updated status is
requested.
Calculates json to store in task_progress field.
"""
task_id = task_result.task_id
result_state = task_result.state
returned_result = task_result.result
result_traceback = task_result.traceback
# Assume we don't always update the CourseTaskLog entry if we don't have to:
entry_needs_saving = False
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
# 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))
if result_state == 'PROGRESS':
# construct a status message directly from the task result's result:
if hasattr(task_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':
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 = returned_result
traceback = result_traceback if result_traceback is not None else ''
entry_needs_saving = True
task_progress = {'exception': type(exception).__name__, 'message': str(exception.message)}
output['message'] = exception.message
log.warning("background task (%s) failed: %s %s".format(task_id, returned_result, traceback))
if result_traceback is not None:
output['task_traceback'] = result_traceback
task_progress['traceback'] = result_traceback
course_task_log_entry.task_progress = json.dumps(task_progress)
output['task_progress'] = task_progress
# 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):
......@@ -169,56 +234,12 @@ def _get_course_task_log_status(task_id):
# Just create the result object, and pull values out once.
# (If we check them later, the state and result may have changed.)
result = AsyncResult(task_id)
result_state = result.state
returned_result = result.result
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:
output.update(_update_course_task_log(course_task_log_entry, result))
elif course_task_log_entry.task_progress is not None:
# 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['task_id'] = course_task_log_entry.task_id
......@@ -274,14 +295,48 @@ def _get_task_completion_message(course_task_log_entry):
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):
"""
Request a problem to be regraded as a background task.
The problem will be regraded 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.
particular problem in a course and have provided and checked an answer.
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 regraded.
......@@ -304,3 +359,67 @@ def submit_regrade_problem_for_all_students(request, course_id, problem_url):
_update_task(course_task_log, task_result)
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
import mitxmako.middleware as middleware
from courseware.models import StudentModule, CourseTaskLog
from courseware.models import StudentModule
from courseware.model_data import ModelDataCache
# from courseware.module_render import get_module
from courseware.module_render import get_module_for_descriptor_internal
......@@ -25,18 +25,6 @@ from track.views import task_track
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
......@@ -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.
"""
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:
# The initialization of Make templating is usually done when Django is
# 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
}
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
......@@ -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,
# 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 {}
task_info = {}
task_info = {"student": student.username, "task_id": xmodule_instance_args['task_id']}
def make_track_function():
'''
......@@ -250,19 +246,21 @@ def _reset_problem_attempts_module_state(module_descriptor, student_module, xmod
@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'
update_fcn = _reset_problem_attempts_module_state
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
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'
update_fcn = _reset_problem_attempts_module_state
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
......@@ -273,19 +271,21 @@ def _delete_problem_module_state(module_descriptor, student_module, xmodule_inst
@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'
update_fcn = _delete_problem_module_state
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
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'
update_fcn = _delete_problem_module_state
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
......
......@@ -10,9 +10,7 @@ import os
import re
import requests
from requests.status_codes import codes
#import urllib
from collections import OrderedDict
#from time import sleep
from StringIO import StringIO
......@@ -25,7 +23,6 @@ from mitxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse
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.access import (has_access, get_access_group_name,
course_beta_test_group_name)
......@@ -43,6 +40,7 @@ import xmodule.graders as xmgraders
import track.views
from .offline_gradecalc import student_grades, offline_grades_available
from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__)
......@@ -139,6 +137,25 @@ def instructor_dashboard(request, course_id):
(group, _) = Group.objects.get_or_create(name=name)
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
action = request.POST.get('action', '')
use_offline = request.POST.get('use_offline_grades', False)
......@@ -177,13 +194,6 @@ def instructor_dashboard(request, course_id):
datatable['title'] = 'List of students enrolled in {0}'.format(course_id)
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:
log.debug(action)
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):
msg += dump_grading_context(course)
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:
course_task_log_entry = task_queue.submit_regrade_problem_for_all_students(request, course_id, problem_url)
if course_task_log_entry is None:
......@@ -224,73 +235,121 @@ def instructor_dashboard(request, course_id):
else:
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')
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:
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
unique_student_identifier = request.POST.get('unique_student_identifier', '')
problem_to_reset = request.POST.get('problem_to_reset', '')
if problem_to_reset[-4:] == ".xml":
problem_to_reset = problem_to_reset[:-4]
problem_urlname = request.POST.get('problem_for_student', '')
module_state_key = get_module_url(problem_urlname)
# try to uniquely id student by email address or username
try:
if "@" in unique_student_identifier:
student_to_reset = User.objects.get(email=unique_student_identifier)
student = User.objects.get(email=unique_student_identifier)
else:
student_to_reset = User.objects.get(username=unique_student_identifier)
msg += "Found a single student to reset. "
except:
student_to_reset = None
student = User.objects.get(username=unique_student_identifier)
msg += "Found a single student. "
except User.DoesNotExist:
student = None
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
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:
(org, course_name, _) = course_id.split("/")
module_state_key = "i4x://" + org + "/" + course_name + "/" + problem_to_reset
module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id,
student_module = StudentModule.objects.get(student_id=student.id,
course_id=course_id,
module_state_key=module_state_key)
msg += "Found module to reset. "
except Exception:
msg += "Found module. "
except StudentModule.DoesNotExist:
msg += "<font color='red'>Couldn't find module with that urlname. </font>"
if "Delete student state for problem" in action:
# delete the state
try:
module_to_reset.delete()
msg += "<font color='red'>Deleted student module state for %s!</font>" % module_state_key
except:
msg += "Failed to delete module state for %s/%s" % (unique_student_identifier, problem_to_reset)
else:
# modify the problem's state
try:
# load the state json
problem_state = json.loads(module_to_reset.state)
old_number_of_attempts = problem_state["attempts"]
problem_state["attempts"] = 0
# save
module_to_reset.state = json.dumps(problem_state)
module_to_reset.save()
track.views.server_track(request,
'{instructor} reset attempts from {old_attempts} to 0 for {student} on problem {problem} in {course}'.format(
old_attempts=old_number_of_attempts,
student=student_to_reset,
problem=module_to_reset.module_state_key,
instructor=request.user,
course=course_id),
{},
page='idashboard')
msg += "<font color='green'>Module state successfully reset!</font>"
except:
msg += "<font color='red'>Couldn't reset module state. </font>"
if student_module is not None:
if "Delete student state for module" in action:
# delete the state
try:
student_module.delete()
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:
msg += "Failed to delete module state for %s/%s" % (unique_student_identifier, problem_urlname)
elif "Reset student's attempts" in action:
# modify the problem's state
try:
# load the state json
problem_state = json.loads(student_module.state)
old_number_of_attempts = problem_state["attempts"]
problem_state["attempts"] = 0
# save
student_module.state = json.dumps(problem_state)
student_module.save()
track.views.server_track(request,
'{instructor} reset attempts from {old_attempts} to 0 for {student} on problem {problem} in {course}'.format(
old_attempts=old_number_of_attempts,
student=student,
problem=student_module.module_state_key,
instructor=request.user,
course=course_id),
{},
page='idashboard')
msg += "<font color='green'>Module state successfully reset!</font>"
except:
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:
unique_student_identifier = request.POST.get('unique_student_identifier', '')
......@@ -308,7 +367,7 @@ def instructor_dashboard(request, course_id):
{},
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)
except:
except User.DoesNotExist:
msg += "<font color='red'>Couldn't find student with that username. </font>"
#----------------------------------------
......
......@@ -15,97 +15,98 @@
(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
if (task_dict.in_progress === true) {
something_in_progress = true;
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
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) {
this.update_progress = __bind(this.update_progress, this);
this.get_status = __bind(this.get_status, this);
this.element = element;
this.entries = $(element).find('.task-progress-entry')
if (window.queuePollerID) {
window.clearTimeout(window.queuePollerID);
}
// find the corresponding entry, and update it:
entry = $(_this.element).find('[data-task-id="' + task_id + '"]');
entry.find('.task-state').text(task_dict.task_state)
var progress_value = task_dict.message || '';
entry.find('.task-progress').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 window.queuePollerID = window.setTimeout(this.get_status, this.initial_refresh_delay);
}
});
};
return CourseTaskProgress;
CourseTaskProgress.prototype.$ = function(selector) {
return $(selector, this.element);
};
CourseTaskProgress.prototype.update_progress = function(response) {
var _this = this;
// Response should be a dict with an entry for each requested task_id,
// with a "task-state" and "in_progress" key and optionally a "message"
// and a "task_progress.duration" key.
var something_in_progress = false;
for (task_id in response) {
var task_dict = response[task_id];
// find the corresponding entry, and update it:
entry = $(_this.element).find('[data-task-id="' + task_id + '"]');
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 || '';
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) {
return window.queuePollerID = window.setTimeout(_this.get_status, _this.refresh_interval);
} else {
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;
})();
}).call(this);
// once the page is rendered, create the progress object
var courseTaskProgress;
$(document).ready(function() {
courseTaskProgress = new CourseTaskProgress($('#task-progress-wrapper'));
});
// once the page is rendered, create the progress object
var courseTaskProgress;
$(document).ready(function() {
courseTaskProgress = new CourseTaskProgress($('#task-progress-wrapper'));
});
</script>
%endif
......@@ -294,25 +295,77 @@ function goto( mode)
<hr width="40%" style="align:left">
%endif
%if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'):
<H2>Course-specific grade adjustment</h2>
<p>to regrade a problem for all students, input the urlname of that problem</p>
<p><input type="text" name="problem_to_regrade" size="60">
<input type="submit" name="action" value="Regrade ALL students' problem submissions">
<p>
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">
</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>
<p>edX email address or their username: </p>
<p><input type="text" name="unique_student_identifier"> <input type="submit" name="action" value="Get link to student's progress page"></p>
<p>and, if you want to reset the number of attempts for a problem, the urlname of that problem
(e.g. if the location is <tt>i4x://university/course/problem/problemname</tt>, then the urlname is <tt>problemname</tt>).</p>
<p> <input type="text" name="problem_to_reset" size="60"> <input type="submit" name="action" value="Reset student's attempts"> </p>
<p>
Specify the edX email address or username of a student here:
<input type="text" name="unique_student_identifier">
</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:
<p> You may also delete the entire state of a student for a problem:
<input type="submit" name="action" value="Delete student state for problem"> </p>
<p>To delete the state of other XBlocks specify modulename/urlname, eg
<tt>combinedopenended/Humanities_SA_Peer</tt></p>
<p>
You may also delete the entire state of a student for the specified module:
<input type="submit" name="action" value="Delete student state for module">
</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
......@@ -484,42 +537,6 @@ function goto( mode)
%if msg:
<p></p><p>${msg}</p>
%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)
##-----------------------------------------------------------------------------
## 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:
<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