diff --git a/lms/djangoapps/courseware/task_queue.py b/lms/djangoapps/courseware/task_queue.py index 946ba99d5e..06522d57e5 100644 --- a/lms/djangoapps/courseware/task_queue.py +++ b/lms/djangoapps/courseware/task_queue.py @@ -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 diff --git a/lms/djangoapps/courseware/tasks.py b/lms/djangoapps/courseware/tasks.py index 5b05eb725d..3ad3b9a830 100644 --- a/lms/djangoapps/courseware/tasks.py +++ b/lms/djangoapps/courseware/tasks.py @@ -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 diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index cf403132d1..f47be688d0 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -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 += '
Celery Status for task ${task}:
Status end.
'.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 += 'Failed to create a background task for regrading "{0}": problem not found.'.format(problem_url) except Exception as e: log.error("Encountered exception from regrade: {0}".format(e)) - msg += 'Failed to create a background task for regrading "{0}": {1}.'.format(problem_url, e) + msg += 'Failed to create a background task for regrading "{0}": {1}.'.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 += 'Failed to create a background task for resetting "{0}".'.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 += 'Failed to create a background task for resetting "{0}": problem not found.'.format(problem_url) + except Exception as e: + log.error("Encountered exception from reset: {0}".format(e)) + msg += 'Failed to create a background task for resetting "{0}": {1}.'.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 += 'Failed to create a background task for deleting "{0}".'.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 += 'Failed to create a background task for deleting state for "{0}": problem not found.'.format(problem_url) + except Exception as e: + log.error("Encountered exception from delete state: {0}".format(e)) + msg += 'Failed to create a background task for deleting state for "{0}": {1}.'.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 += "Couldn't find student with that email or username. " - 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 += "Couldn't find module with that urlname. " - if "Delete student state for problem" in action: - # delete the state - try: - module_to_reset.delete() - msg += "Deleted student module state for %s!" % 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 += "Deleted student module state for %s!" % 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 += "Module state successfully reset!" - except: - msg += "Couldn't reset module state. " + # 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 += "Module state successfully reset!" + except: + msg += "Couldn't reset module state. " + 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 += 'Failed to create a background task for regrading "{0}" for student {1}.'.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 += 'Failed to create a background task for regrading "{0}": {1}.'.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 += " Progress page for username: {1} with email address: {2}.".format(progress_url, student_to_reset.username, student_to_reset.email) - except: + except User.DoesNotExist: msg += "Couldn't find student with that username. " #---------------------------------------- diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index acc32841be..c5c7217c0f 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -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')); +}); %endif @@ -294,25 +295,77 @@ function goto( mode)to regrade a problem for all students, input the urlname of that problem
-- +
+ Specify a particular problem in the course here by its url: + +
++ You may use just the "urlname" if a problem, or "modulename/urlname" if not. + (For example, if the location is i4x://university/course/problem/problemname, + then just provide the problemname. + If the location is i4x://university/course/notaproblem/someothername, then + provide notaproblem/someothername.) +
++ Then select an action: + + +
++
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: +
++
+edX email address or their username:
--
and, if you want to reset the number of attempts for a problem, the urlname of that problem - (e.g. if the location is i4x://university/course/problem/problemname, then the urlname is problemname).
-+
+ Specify the edX email address or username of a student here: + +
++ Click this, and a link to student's progress page will appear below: + +
++ Specify a particular problem in the course here by its url: + +
++ You may use just the "urlname" if a problem, or "modulename/urlname" if not. + (For example, if the location is i4x://university/course/problem/problemname, + then just provide the problemname. + If the location is i4x://university/course/notaproblem/someothername, then + provide notaproblem/someothername.) +
++ Then select an action: + + %if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'): + + %endif +
%if instructor_access: -You may also delete the entire state of a student for a problem: -
-To delete the state of other XBlocks specify modulename/urlname, eg - combinedopenended/Humanities_SA_Peer
++ You may also delete the entire state of a student for the specified module: + +
+ %endif + %if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'): +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: +
++ +
%endif %endif @@ -484,42 +537,6 @@ function goto( mode) %if msg:${msg}
%endif -##----------------------------------------------------------------------------- -## Output tasks in progress - -%if course_tasks is not None and len(course_tasks) > 0: -Pending Course Tasks
-| Task Name | -Task Arg | -Student | -Task Id | -Requester | -Submitted | -Last Update | -Task State | -Task Progress | -
|---|---|---|---|---|---|---|---|---|
| ${course_task.task_name} | -${course_task.task_args} | -${course_task.student} | -${course_task.task_id} |
- ${course_task.requester} | -${course_task.created} | -${course_task.updated} |
- ${course_task.task_state} |
- unknown |
-
| Task Name | +Task Arg | +Student | +Task Id | +Requester | +Submitted | +Task State | +Duration (ms) | +Task Progress | +
|---|---|---|---|---|---|---|---|---|
| ${course_task.task_name} | +${course_task.task_args} | +${course_task.student} | +${course_task.task_id} |
+ ${course_task.requester} | +${course_task.created} | +${course_task.task_state} |
+ unknown |
+ unknown |
+