Add duration to task status. Add tests for reset-attempts.
This commit is contained in:
@@ -224,6 +224,7 @@ def _get_course_task_log_status(task_id):
|
||||
'updated': number of attempts that "succeeded"
|
||||
'total': number of possible subtasks to attempt
|
||||
'action_name': user-visible verb to use in status messages. Should be past-tense.
|
||||
'duration_ms': how long the task has (or had) been running.
|
||||
'task_traceback': optional, returned if task failed and produced a traceback.
|
||||
'succeeded': on complete tasks, indicates if the task outcome was successful:
|
||||
did it achieve what it set out to do.
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
|
||||
import json
|
||||
from time import sleep
|
||||
from time import sleep, time
|
||||
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import transaction
|
||||
|
||||
from celery import task, current_task
|
||||
# from celery.signals import worker_ready
|
||||
from celery.utils.log import get_task_logger
|
||||
|
||||
import mitxmako.middleware as middleware
|
||||
|
||||
from courseware.models import StudentModule
|
||||
from courseware.model_data import ModelDataCache
|
||||
# from courseware.module_render import get_module
|
||||
from courseware.module_render import get_module_for_descriptor_internal
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -40,14 +39,18 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc
|
||||
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))
|
||||
|
||||
# get start time for task:
|
||||
start_time = time()
|
||||
|
||||
# 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
|
||||
|
||||
# Hack to get mako templates to work on celery worker server's worker thread.
|
||||
# The initialization of Mako templating is usually done when Django is
|
||||
# initializing middleware packages as part of processing a server request.
|
||||
# When this is run on a celery worker server, no such initialization is
|
||||
# called. So we look for the result: the defining of the lookup paths
|
||||
# called. Using @worker_ready.connect doesn't run in the right container.
|
||||
# So we look for the result: the defining of the lookup paths
|
||||
# for templates.
|
||||
if 'main' not in middleware.lookup:
|
||||
task_log.info("Initializing Mako middleware explicitly")
|
||||
@@ -74,14 +77,16 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc
|
||||
num_total = len(modules_to_update) # TODO: make this more efficient. Count()?
|
||||
|
||||
def get_task_progress():
|
||||
current_time = time()
|
||||
progress = {'action_name': action_name,
|
||||
'attempted': num_attempted,
|
||||
'updated': num_updated,
|
||||
'total': num_total,
|
||||
'start_ms': int(start_time * 1000),
|
||||
'duration_ms': int((current_time - start_time) * 1000),
|
||||
}
|
||||
return progress
|
||||
|
||||
|
||||
for module_to_update in modules_to_update:
|
||||
num_attempted += 1
|
||||
# There is no try here: if there's an error, we let it throw, and the task will
|
||||
@@ -102,7 +107,8 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc
|
||||
task_progress = get_task_progress()
|
||||
current_task.update_state(state='PROGRESS', meta=task_progress)
|
||||
|
||||
task_log.info("Finished processing task")
|
||||
fmt = 'Finishing task "{task_id}": course "{course_id}" problem "{state_key}": final: {progress}'
|
||||
task_log.info(fmt.format(task_id=task_id, course_id=course_id, state_key=module_state_key, progress=task_progress))
|
||||
return task_progress
|
||||
|
||||
|
||||
@@ -288,11 +294,15 @@ def delete_problem_state_for_all_students(course_id, problem_url, xmodule_instan
|
||||
xmodule_instance_args=xmodule_instance_args)
|
||||
|
||||
|
||||
# Using @worker_ready.connect was an effort to call middleware initialization
|
||||
# only once, when the worker was coming up. However, the actual worker task
|
||||
# was not getting initialized, so it was likely running in a separate process
|
||||
# from the worker server.
|
||||
#@worker_ready.connect
|
||||
#def initialize_middleware(**kwargs):
|
||||
# # The initialize Django middleware - some middleware components
|
||||
# # Initialize Django middleware - some middleware components
|
||||
# # are initialized lazily when the first request is served. Since
|
||||
# # the celery workers do not serve request, the components never
|
||||
# # the celery workers do not serve requests, the components never
|
||||
# # get initialized, causing errors in some dependencies.
|
||||
# # In particular, the Mako template middleware is used by some xmodules
|
||||
# task_log.info("Initializing all middleware from worker_ready.connect hook")
|
||||
|
||||
@@ -19,7 +19,8 @@ from student.tests.factories import CourseEnrollmentFactory, UserFactory, AdminF
|
||||
from courseware.model_data import StudentModule
|
||||
from courseware.task_queue import (submit_regrade_problem_for_all_students,
|
||||
submit_regrade_problem_for_student,
|
||||
course_task_log_status)
|
||||
course_task_log_status,
|
||||
submit_reset_problem_attempts_for_all_students)
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MONGO_MODULESTORE
|
||||
|
||||
|
||||
@@ -228,7 +229,7 @@ class TestRegrading(TestRegradingBase):
|
||||
self.create_student('u4')
|
||||
self.logout()
|
||||
|
||||
def testRegradingOptionProblem(self):
|
||||
def test_regrading_option_problem(self):
|
||||
'''Run regrade scenario on option problem'''
|
||||
# get descriptor:
|
||||
problem_url_name = 'H1P1'
|
||||
@@ -280,7 +281,7 @@ class TestRegrading(TestRegradingBase):
|
||||
display_name=str(problem_url_name),
|
||||
data=problem_xml)
|
||||
|
||||
def testRegradingFailure(self):
|
||||
def test_regrading_failure(self):
|
||||
"""Simulate a failure in regrading a problem"""
|
||||
problem_url_name = 'H1P1'
|
||||
self.define_option_problem(problem_url_name)
|
||||
@@ -307,19 +308,19 @@ class TestRegrading(TestRegradingBase):
|
||||
status = json.loads(response.content)
|
||||
self.assertEqual(status['message'], expected_message)
|
||||
|
||||
def testRegradingNonProblem(self):
|
||||
def test_regrading_non_problem(self):
|
||||
"""confirm that a non-problem will not submit"""
|
||||
problem_url_name = self.problem_section.location.url()
|
||||
with self.assertRaises(NotImplementedError):
|
||||
self.regrade_all_student_answers('instructor', problem_url_name)
|
||||
|
||||
def testRegradingNonexistentProblem(self):
|
||||
def test_regrading_nonexistent_problem(self):
|
||||
"""confirm that a non-existent problem will not submit"""
|
||||
problem_url_name = 'NonexistentProblem'
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self.regrade_all_student_answers('instructor', problem_url_name)
|
||||
|
||||
def testRegradingCodeProblem(self):
|
||||
def test_regrading_code_problem(self):
|
||||
'''Run regrade scenario on problem with code submission'''
|
||||
problem_url_name = 'H1P2'
|
||||
self.define_code_response_problem(problem_url_name)
|
||||
@@ -338,3 +339,84 @@ class TestRegrading(TestRegradingBase):
|
||||
response = course_task_log_status(mock_request, task_id=course_task_log.task_id)
|
||||
status = json.loads(response.content)
|
||||
self.assertEqual(status['message'], "Problem's definition does not support regrading")
|
||||
|
||||
|
||||
class TestResetAttempts(TestRegradingBase):
|
||||
userlist = ['u1', 'u2', 'u3', 'u4']
|
||||
|
||||
def setUp(self):
|
||||
self.initialize_course()
|
||||
self.create_instructor('instructor')
|
||||
for username in self.userlist:
|
||||
self.create_student(username)
|
||||
self.logout()
|
||||
|
||||
def get_num_attempts(self, username, descriptor):
|
||||
module = self.get_student_module(username, descriptor)
|
||||
state = json.loads(module.state)
|
||||
return state['attempts']
|
||||
|
||||
def reset_problem_attempts(self, instructor, problem_url_name):
|
||||
"""Submits the current problem for resetting"""
|
||||
return submit_reset_problem_attempts_for_all_students(self._create_task_request(instructor), self.course.id,
|
||||
TestRegradingBase.problem_location(problem_url_name))
|
||||
|
||||
def test_reset_attempts_on_problem(self):
|
||||
'''Run reset-attempts scenario on option problem'''
|
||||
# get descriptor:
|
||||
problem_url_name = 'H1P1'
|
||||
self.define_option_problem(problem_url_name)
|
||||
location = TestRegradingBase.problem_location(problem_url_name)
|
||||
descriptor = self.module_store.get_instance(self.course.id, location)
|
||||
num_attempts = 3
|
||||
# first store answers for each of the separate users:
|
||||
for _ in range(num_attempts):
|
||||
for username in self.userlist:
|
||||
self.submit_student_answer(username, problem_url_name, ['Option 1', 'Option 1'])
|
||||
|
||||
for username in self.userlist:
|
||||
self.assertEquals(self.get_num_attempts(username, descriptor), num_attempts)
|
||||
|
||||
self.reset_problem_attempts('instructor', problem_url_name)
|
||||
|
||||
for username in self.userlist:
|
||||
self.assertEquals(self.get_num_attempts(username, descriptor), 0)
|
||||
|
||||
def test_reset_failure(self):
|
||||
"""Simulate a failure in resetting attempts on a problem"""
|
||||
problem_url_name = 'H1P1'
|
||||
self.define_option_problem(problem_url_name)
|
||||
self.submit_student_answer('u1', problem_url_name, ['Option 1', 'Option 1'])
|
||||
|
||||
expected_message = "bad things happened"
|
||||
with patch('courseware.models.StudentModule.save') as mock_save:
|
||||
mock_save.side_effect = ZeroDivisionError(expected_message)
|
||||
course_task_log = self.reset_problem_attempts('instructor', problem_url_name)
|
||||
|
||||
# check task_log returned
|
||||
self.assertEqual(course_task_log.task_state, 'FAILURE')
|
||||
self.assertEqual(course_task_log.student, None)
|
||||
self.assertEqual(course_task_log.requester.username, 'instructor')
|
||||
self.assertEqual(course_task_log.task_name, 'reset_problem_attempts')
|
||||
self.assertEqual(course_task_log.task_args, TestRegrading.problem_location(problem_url_name))
|
||||
status = json.loads(course_task_log.task_progress)
|
||||
self.assertEqual(status['exception'], 'ZeroDivisionError')
|
||||
self.assertEqual(status['message'], expected_message)
|
||||
|
||||
# check status returned:
|
||||
mock_request = Mock()
|
||||
response = course_task_log_status(mock_request, task_id=course_task_log.task_id)
|
||||
status = json.loads(response.content)
|
||||
self.assertEqual(status['message'], expected_message)
|
||||
|
||||
def test_reset_non_problem(self):
|
||||
"""confirm that a non-problem can still be successfully reset"""
|
||||
problem_url_name = self.problem_section.location.url()
|
||||
course_task_log = self.reset_problem_attempts('instructor', problem_url_name)
|
||||
self.assertEqual(course_task_log.task_state, 'SUCCESS')
|
||||
|
||||
def test_reset_nonexistent_problem(self):
|
||||
"""confirm that a non-existent problem will not submit"""
|
||||
problem_url_name = 'NonexistentProblem'
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self.reset_problem_attempts('instructor', problem_url_name)
|
||||
|
||||
@@ -1308,25 +1308,34 @@ def get_background_task_table(course_id, problem_url, student=None):
|
||||
"Task Id",
|
||||
"Requester",
|
||||
"Submitted",
|
||||
"Updated",
|
||||
"Duration",
|
||||
"Task State",
|
||||
"Task Status",
|
||||
"Message"]
|
||||
|
||||
datatable['data'] = []
|
||||
for i, course_task in enumerate(history_entries):
|
||||
# get duration info, if known:
|
||||
duration_ms = 'unknown'
|
||||
if hasattr(course_task, 'task_progress'):
|
||||
task_progress = json.loads(course_task.task_progress)
|
||||
if 'duration_ms' in task_progress:
|
||||
duration_ms = task_progress['duration_ms']
|
||||
# get progress status message:
|
||||
success, message = task_queue.get_task_completion_message(course_task)
|
||||
if success:
|
||||
status = "Complete"
|
||||
else:
|
||||
status = "Incomplete"
|
||||
# generate row for this task:
|
||||
row = ["#{0}".format(len(history_entries) - i),
|
||||
str(course_task.task_name),
|
||||
str(course_task.student),
|
||||
str(course_task.task_id),
|
||||
str(course_task.requester),
|
||||
course_task.created.strftime("%Y/%m/%d %H:%M:%S"),
|
||||
course_task.updated.strftime("%Y/%m/%d %H:%M:%S"),
|
||||
duration_ms,
|
||||
#course_task.updated.strftime("%Y/%m/%d %H:%M:%S"),
|
||||
str(course_task.task_state),
|
||||
status,
|
||||
message]
|
||||
|
||||
Reference in New Issue
Block a user