Add additional background tasks: reset attempts, delete state. Update
CourseTaskLog fully after task submission, so it works in Eager mode (for testing).
This commit is contained in:
@@ -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.
|
||||
Updates and possibly saves a CourseTaskLog entry based on a task Result.
|
||||
|
||||
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.
|
||||
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 student's attempts" in action or "Delete student state for problem" in action:
|
||||
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 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
|
||||
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
|
||||
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>"
|
||||
# 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); }; };
|
||||
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
|
||||
this.CourseTaskProgress = (function() {
|
||||
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();
|
||||
}
|
||||
// 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;
|
||||
|
||||
CourseTaskProgress.prototype.reinitialize = function(element) {
|
||||
this.entries = $(element).find('.task-progress-entry')
|
||||
};
|
||||
// Hardcode the initial delay, for the first refresh, to two seconds:
|
||||
var initial_refresh_delay = 2000;
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
return window.queuePollerID = window.setTimeout(this.get_status, this.initial_refresh_delay);
|
||||
}
|
||||
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;
|
||||
|
||||
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 = [];
|
||||
|
||||
return CourseTaskProgress;
|
||||
// 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/>
|
||||
|
||||
Reference in New Issue
Block a user