Commit 91ac6e68 by Brian Wilson

Define initial celery task on instructor dash.

Add CourseTaskLog, and make calls to regrading.

Add regrading implementation, and hack the marshalling of request.
parent 94238238
......@@ -270,7 +270,26 @@ class LoncapaProblem(object):
# if answers include File objects, convert them to filenames.
self.student_answers = convert_files_to_filenames(answers)
return self._grade_answers(answers)
def regrade_existing_answers(self):
'''
Regrade student responses. Called by capa_module.regrade_problem.
'''
return self._grade_answers(None)
def _grade_answers(self, answers):
'''
Internal grading call used for checking new student answers and also
regrading existing student answers.
answers is a dict of all the entries from request.POST, but with the first part
of each key removed (the string before the first "_").
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123
Calls the Response for each question in this problem, to do the actual grading.
'''
# old CorrectMap
oldcmap = self.correct_map
......@@ -281,10 +300,11 @@ class LoncapaProblem(object):
for responder in self.responders.values():
# File objects are passed only if responsetype explicitly allows for file
# submissions
if 'filesubmission' in responder.allowed_inputfields:
# TODO: figure out where to get file submissions when regrading.
if 'filesubmission' in responder.allowed_inputfields and answers is not None:
results = responder.evaluate_answers(answers, oldcmap)
else:
results = responder.evaluate_answers(convert_files_to_filenames(answers), oldcmap)
results = responder.evaluate_answers(self.student_answers, oldcmap)
newcmap.update(results)
self.correct_map = newcmap
# log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap))
......
......@@ -759,6 +759,8 @@ class CapaModule(CapaFields, XModule):
try:
correct_map = self.lcp.grade_answers(answers)
self.attempts = self.attempts + 1
self.lcp.done = True
self.set_state_from_lcp()
except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
......@@ -785,10 +787,6 @@ class CapaModule(CapaFields, XModule):
return {'success': msg}
raise
self.attempts = self.attempts + 1
self.lcp.done = True
self.set_state_from_lcp()
self.publish_grade()
# success = correct if ALL questions in this problem are correct
......@@ -814,6 +812,63 @@ class CapaModule(CapaFields, XModule):
'contents': html,
}
def regrade_problem(self):
''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers:
{'success' : 'correct' | 'incorrect' | AJAX alert msg string,
'contents' : html}
'''
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url()
if not self.done:
event_info['failure'] = 'unanswered'
self.system.track_function('save_problem_regrade_fail', event_info)
raise NotFoundError('Problem must be answered before it can be graded again')
try:
correct_map = self.lcp.regrade_existing_answers()
# regrading should have no effect on attempts, so don't
# need to increment here, or mark done. Just save.
self.set_state_from_lcp()
except StudentInputError as inst:
log.exception("StudentInputError in capa_module:problem_regrade")
return {'success': inst.message}
except Exception, err:
if self.system.DEBUG:
msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc()
return {'success': msg}
raise
self.publish_grade()
# success = correct if ALL questions in this problem are correct
success = 'correct'
for answer_id in correct_map:
if not correct_map.is_correct(answer_id):
success = 'incorrect'
# NOTE: We are logging both full grading and queued-grading submissions. In the latter,
# 'success' will always be incorrect
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
event_info['attempts'] = self.attempts
self.system.track_function('save_problem_regrade', event_info)
# TODO: figure out if psychometrics should be called on regrading requests
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_instance_state())
# render problem into HTML
html = self.get_problem_html(encapsulate=False)
return {'success': success,
'contents': html,
}
def save_problem(self, get):
'''
Save the passed in answers.
......
......@@ -17,6 +17,7 @@ from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
class StudentModule(models.Model):
"""
Keeps student state for a particular module in a particular course.
......@@ -262,3 +263,20 @@ class OfflineComputedGradeLog(models.Model):
def __unicode__(self):
return "[OCGLog] %s: %s" % (self.course_id, self.created)
class CourseTaskLog(models.Model):
"""
Stores information about background tasks that have been submitted to
perform course-specific work.
Examples include grading and regrading.
"""
course_id = models.CharField(max_length=255, db_index=True)
student = models.ForeignKey(User, null=True, db_index=True, related_name='+') # optional: None = task applies to all students
task_name = models.CharField(max_length=50, db_index=True)
task_args = models.CharField(max_length=255, db_index=True)
task_id = models.CharField(max_length=255, db_index=True) # max_length from celery_taskmeta
task_status = models.CharField(max_length=50, null=True, db_index=True) # max_length from celery_taskmeta
requester = models.ForeignKey(User, db_index=True, related_name='+')
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
......@@ -12,6 +12,7 @@ import requests
from requests.status_codes import codes
import urllib
from collections import OrderedDict
from time import sleep
from StringIO import StringIO
......@@ -24,6 +25,7 @@ from mitxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse
from courseware import grades
from courseware import tasks
from courseware.access import (has_access, get_access_group_name,
course_beta_test_group_name)
from courseware.courses import get_course_with_access
......@@ -174,6 +176,13 @@ 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)
......@@ -205,6 +214,13 @@ def instructor_dashboard(request, course_id):
track.views.server_track(request, action, {}, page='idashboard')
msg += dump_grading_context(course)
elif "Regrade ALL students' problem submissions" in action:
problem_url = request.POST.get('problem_to_regrade', '')
try:
result = tasks.regrade_problem_for_all_students(request, course_id, problem_url)
except Exception as e:
log.error("Encountered exception from regrade: {msg}", msg=e.message())
elif "Reset student's attempts" in action or "Delete student state for problem" in action:
# get the form data
unique_student_identifier = request.POST.get('unique_student_identifier', '')
......@@ -1181,3 +1197,101 @@ def dump_grading_context(course):
msg += "length=%d\n" % len(gc['all_descriptors'])
msg = '<pre>%s</pre>' % msg.replace('<', '&lt;')
return msg
def old1testcelery(request):
"""
A Simple view that checks if the application can talk to the celery workers
"""
args = ('ping',)
result = tasks.echo.apply_async(args, retry=False)
value = result.get(timeout=0.5)
output = {
'task_id': result.id,
'value': value
}
return HttpResponse(json.dumps(output, indent=4))
def old2testcelery(request):
"""
A Simple view that checks if the application can talk to the celery workers
"""
args = (10,)
result = tasks.waitawhile.apply_async(args, retry=False)
while not result.ready():
sleep(0.5) # in seconds
if result.state == "PROGRESS":
if hasattr(result, 'result') and 'current' in result.result:
log.info("still waiting... progress at {0} of {1}".format(result.result['current'], result.result['total']))
else:
log.info("still making progress... ")
if result.successful():
value = result.result
output = {
'task_id': result.id,
'value': value
}
return HttpResponse(json.dumps(output, indent=4))
def testcelery(request):
"""
A Simple view that checks if the application can talk to the celery workers
"""
args = (10,)
result = tasks.waitawhile.apply_async(args, retry=False)
task_id = result.id
# return the task_id to a template which will set up an ajax call to
# check the progress of the task.
return testcelery_status(request, task_id)
# return mitxmako.shortcuts.render_to_response('celery_ajax.html', {
# 'element_id': 'celery_task'
# 'id': self.task_id,
# 'ajax_url': reverse('testcelery_ajax'),
# })
def testcelery_status(request, task_id):
result = tasks.waitawhile.AsyncResult(task_id)
while not result.ready():
sleep(0.5) # in seconds
if result.state == "PROGRESS":
if hasattr(result, 'result') and 'current' in result.result:
log.info("still waiting... progress at {0} of {1}".format(result.result['current'], result.result['total']))
else:
log.info("still making progress... ")
if result.successful():
value = result.result
output = {
'task_id': result.id,
'value': value
}
return HttpResponse(json.dumps(output, indent=4))
def celery_task_status(request, task_id):
# TODO: determine if we need to know the name of the original task,
# or if this could be any task... Sample code seems to indicate that
# we could just include the AsyncResult class directly, i.e.:
# from celery.result import AsyncResult.
result = tasks.waitawhile.AsyncResult(task_id)
output = {
'task_id': result.id,
'state': result.state
}
if result.state == "PROGRESS":
if hasattr(result, 'result') and 'current' in result.result:
log.info("still waiting... progress at {0} of {1}".format(result.result['current'], result.result['total']))
output['current'] = result.result['current']
output['total'] = result.result['total']
else:
log.info("still making progress... ")
if result.successful():
value = result.result
output['value'] = value
return HttpResponse(json.dumps(output, indent=4))
......@@ -194,6 +194,12 @@ function goto( mode)
<hr width="40%" style="align:left">
%endif
<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>
<H2>Student-specific grade inspection and adjustment</h2>
<p>edX email address or their username: </p>
......@@ -234,6 +240,7 @@ function goto( mode)
##-----------------------------------------------------------------------------
%if modeflag.get('Admin'):
%if instructor_access:
<hr width="40%" style="align:left">
<p>
......@@ -331,6 +338,10 @@ function goto( mode)
##-----------------------------------------------------------------------------
%if modeflag.get('Data'):
<p>
<input type="submit" name="action" value="Test Celery">
<p>
<hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="Download CSV of all student profile data">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment