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.
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CourseTaskLog'
db.create_table('courseware_coursetasklog', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('student', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', null=True, to=orm['auth.User'])),
('task_name', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('task_args', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('task_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('task_status', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, db_index=True)),
('requester', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', to=orm['auth.User'])),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
))
db.send_create_signal('courseware', ['CourseTaskLog'])
def backwards(self, orm):
# Deleting model 'CourseTaskLog'
db.delete_table('courseware_coursetasklog')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'courseware.coursetasklog': {
'Meta': {'object_name': 'CourseTaskLog'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'requester': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['auth.User']"}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}),
'task_args': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'task_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'task_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'task_status': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'db_index': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'})
},
'courseware.offlinecomputedgrade': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'courseware.offlinecomputedgradelog': {
'Meta': {'ordering': "['-created']", 'object_name': 'OfflineComputedGradeLog'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'})
},
'courseware.studentmodule': {
'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}),
'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'courseware.studentmodulehistory': {
'Meta': {'object_name': 'StudentModuleHistory'},
'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
'grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'student_module': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courseware.StudentModule']"}),
'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'})
},
'courseware.xmodulecontentfield': {
'Meta': {'unique_together': "(('definition_id', 'field_name'),)", 'object_name': 'XModuleContentField'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'definition_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
},
'courseware.xmodulesettingsfield': {
'Meta': {'unique_together': "(('usage_id', 'field_name'),)", 'object_name': 'XModuleSettingsField'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'usage_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
},
'courseware.xmodulestudentinfofield': {
'Meta': {'unique_together': "(('student', 'field_name'),)", 'object_name': 'XModuleStudentInfoField'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
},
'courseware.xmodulestudentprefsfield': {
'Meta': {'unique_together': "(('student', 'module_type', 'field_name'),)", 'object_name': 'XModuleStudentPrefsField'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'module_type': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
}
}
complete_apps = ['courseware']
......@@ -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)
import json
import logging
from django.contrib.auth.models import User
from courseware.models import StudentModule, CourseTaskLog
from courseware.model_data import ModelDataCache
from courseware.module_render import get_module
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError,\
InvalidLocationError
import track.views
from celery import task, current_task
from celery.utils.log import get_task_logger
from time import sleep
from django.core.handlers.wsgi import WSGIRequest
logger = get_task_logger(__name__)
# celery = Celery('tasks', broker='django://')
log = logging.getLogger(__name__)
@task
def add(x, y):
return x + y
@task
def echo(value):
if value == 'ping':
result = 'pong'
else:
result = 'got: {0}'.format(value)
return result
@task
def waitawhile(value):
for i in range(value):
sleep(1) # in seconds
logger.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
def _update_problem_module_state(request, course_id, problem_url, student, update_fcn, action_name, filter_fcn):
'''
Performs generic update by visiting StudentModule instances with the update_fcn provided
If student is None, performs update on modules for all students on the specified problem
'''
module_state_key = problem_url
# TODO: store this in the task state, not as a separate return value.
# (Unless that's not what the task state is intended to mean. The task can successfully
# complete, as far as celery is concerned, but have an internal status of failed.)
succeeded = False
# find the problem descriptor, if any:
try:
module_descriptor = modulestore().get_instance(course_id, module_state_key)
succeeded = True
except ItemNotFoundError:
msg = "Couldn't find problem with that urlname."
except InvalidLocationError:
msg = "Couldn't find problem with that urlname."
if module_descriptor is None:
msg = "Couldn't find problem with that urlname."
# if not succeeded:
# current_task.update_state(
# meta={'attempted': num_attempted, 'updated': num_updated, 'total': num_total})
# The task should still succeed, but should have metadata indicating
# that the result of the successful task was a failure. (It's not
# the queue that failed, but the task put on the queue.)
# find the module in question
succeeded = False
modules_to_update = StudentModule.objects.filter(course_id=course_id,
module_state_key=module_state_key)
# give the option of regrading an individual student. If not specified,
# then regrades all students who have responded to a problem so far
if student is not None:
modules_to_update = modules_to_update.filter(student_id=student.id)
if filter_fcn is not None:
modules_to_update = filter_fcn(modules_to_update)
# perform the main loop
num_updated = 0
num_attempted = 0
num_total = len(modules_to_update) # TODO: make this more efficient. Count()?
for module_to_update in modules_to_update:
num_attempted += 1
# try:
if update_fcn(request, module_to_update, module_descriptor):
num_updated += 1
# if there's an error, just let it throw, and the task will
# be marked as FAILED, with a stack trace.
# except UpdateProblemModuleStateError as e:
# something bad happened, so exit right away
# return (succeeded, e.message)
# update task status:
current_task.update_state(state='PROGRESS',
meta={'attempted': num_attempted, 'updated': num_updated, 'total': num_total})
# done with looping through all modules, so just return final statistics:
if student is not None:
if num_attempted == 0:
msg = "Unable to find submission to be {action} for student '{student}' and problem '{problem}'."
elif num_updated == 0:
msg = "Problem failed to be {action} for student '{student}' and problem '{problem}'!"
else:
succeeded = True
msg = "Problem successfully {action} for student '{student}' and problem '{problem}'"
elif num_attempted == 0:
msg = "Unable to find any students with submissions to be {action} for problem '{problem}'."
elif num_updated == 0:
msg = "Problem failed to be {action} for any of {attempted} students for problem '{problem}'!"
elif num_updated == num_attempted:
succeeded = True
msg = "Problem successfully {action} for {attempted} students for problem '{problem}'!"
elif num_updated < num_attempted:
msg = "Problem {action} for {updated} of {attempted} students for problem '{problem}'!"
msg = msg.format(action=action_name, updated=num_updated, attempted=num_attempted, student=student, problem=module_state_key)
# update status in task result object itself:
current_task.update_state(state='DONE',
meta={'attempted': num_attempted, 'updated': num_updated, 'total': num_total,
'succeeded': succeeded, 'message': msg})
# and update status in course task table as well:
# TODO: figure out how this is legal. The actual task result
# status is updated by celery when this task completes, and is
# not
# course_task_log_entry = CourseTaskLog.objects.get(task_id=current_task.id)
# course_task_log_entry.task_status = ...
# return (succeeded, msg)
return succeeded
def _update_problem_module_state_for_student(request, course_id, problem_url, student_identifier,
update_fcn, action_name, filter_fcn=None):
msg = ''
success = False
# try to uniquely id student by email address or username
try:
if "@" in student_identifier:
student_to_update = User.objects.get(email=student_identifier)
elif student_identifier is not None:
student_to_update = User.objects.get(username=student_identifier)
return _update_problem_module_state(request, course_id, problem_url, student_to_update, update_fcn, action_name, filter_fcn)
except User.DoesNotExist:
msg = "Couldn't find student with that email or username."
return (success, msg)
def _update_problem_module_state_for_all_students(request, course_id, problem_url, update_fcn, action_name, filter_fcn=None):
return _update_problem_module_state(request, course_id, problem_url, None, update_fcn, action_name, filter_fcn)
def _regrade_problem_module_state(request, module_to_regrade, module_descriptor):
'''
Takes an XModule descriptor and a corresponding StudentModule object, and
performs regrading on the student's problem submission.
Throws exceptions if the regrading is fatal and should be aborted if in a loop.
'''
# unpack the StudentModule:
course_id = module_to_regrade.course_id
student = module_to_regrade.student
module_state_key = module_to_regrade.module_state_key
# reconstitute the problem's corresponding XModule:
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, student,
module_descriptor)
# Note that the request is passed to get_module() to provide xqueue-related URL information
instance = get_module(student, request, module_state_key, model_data_cache,
course_id, grade_bucket_type='regrade')
if instance is None:
# Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to.
msg = "No module {loc} for student {student}--access denied?".format(loc=module_state_key,
student=student)
log.debug(msg)
raise UpdateProblemModuleStateError(msg)
if not hasattr(instance, 'regrade_problem'):
# if the first instance doesn't have a regrade method, we should
# probably assume that no other instances will either.
msg = "Specified problem does not support regrading."
raise UpdateProblemModuleStateError(msg)
result = instance.regrade_problem()
if 'success' not in result:
# don't consider these fatal, but false means that the individual call didn't complete:
log.debug("error processing regrade call for problem {loc} and student {student}: "
"unexpected response {msg}".format(msg=result, loc=module_state_key, student=student))
return False
elif result['success'] != 'correct' and result['success'] != 'incorrect':
log.debug("error processing regrade call for problem {loc} and student {student}: "
"{msg}".format(msg=result['success'], loc=module_state_key, student=student))
return False
else:
track.views.server_track(request,
'{instructor} regrade problem {problem} for student {student} '
'in {course}'.format(student=student.id,
problem=module_to_regrade.module_state_key,
instructor=request.user,
course=course_id),
{},
page='idashboard')
return True
def filter_problem_module_state_for_done(modules_to_update):
return modules_to_update.filter(state__contains='"done": true')
@task
def _regrade_problem_for_student(request, course_id, problem_url, student_identifier):
action_name = 'regraded'
update_fcn = _regrade_problem_module_state
filter_fcn = filter_problem_module_state_for_done
return _update_problem_module_state_for_student(request, course_id, problem_url, student_identifier,
update_fcn, action_name, filter_fcn)
def regrade_problem_for_student(request, course_id, problem_url, student_identifier):
# First submit task. Then put stuff into table with the resulting task_id.
result = _regrade_problem_for_student.apply_async(request, course_id, problem_url, student_identifier)
task_id = result.id
# TODO: for log, would want student_identifier to already be mapped to the student
tasklog_args = {'course_id': course_id,
'task_name': 'regrade',
'task_args': problem_url,
'task_id': task_id,
'task_status': result.state,
'requester': request.user}
CourseTaskLog.objects.create(**tasklog_args)
return result
@task
def _regrade_problem_for_all_students(request_environ, course_id, problem_url):
# request = dummy_request
request = WSGIRequest(request_environ)
action_name = 'regraded'
update_fcn = _regrade_problem_module_state
filter_fcn = filter_problem_module_state_for_done
return _update_problem_module_state_for_all_students(request, course_id, problem_url,
update_fcn, action_name, filter_fcn)
def regrade_problem_for_all_students(request, course_id, problem_url):
# Figure out (for now) how to serialize what we need of the request. The actual
# request will not successfully serialize with json or with pickle.
request_environ = {'HTTP_USER_AGENT': request.META['HTTP_USER_AGENT'],
'REMOTE_ADDR': request.META['REMOTE_ADDR'],
'SERVER_NAME': request.META['SERVER_NAME'],
'REQUEST_METHOD': 'GET',
# 'HTTP_X_FORWARDED_PROTO': request.META['HTTP_X_FORWARDED_PROTO'],
}
# Submit task. Then put stuff into table with the resulting task_id.
task_args = [request_environ, course_id, problem_url]
result = _regrade_problem_for_all_students.apply_async(task_args)
task_id = result.id
tasklog_args = {'course_id': course_id,
'task_name': 'regrade',
'task_args': problem_url,
'task_id': task_id,
'task_status': result.state,
'requester': request.user}
course_task_log = CourseTaskLog.objects.create(**tasklog_args)
return course_task_log
def _reset_problem_attempts_module_state(request, module_to_reset, module_descriptor):
# modify the problem's state
# load the state json and change state
problem_state = json.loads(module_to_reset.state)
if 'attempts' in problem_state:
old_number_of_attempts = problem_state["attempts"]
if old_number_of_attempts > 0:
problem_state["attempts"] = 0
# convert back to json and save
module_to_reset.state = json.dumps(problem_state)
module_to_reset.save()
# write out tracking info
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=module_to_reset.student,
problem=module_to_reset.module_state_key,
instructor=request.user,
course=module_to_reset.course_id),
{},
page='idashboard')
# consider the reset to be successful, even if no update was performed. (It's just "optimized".)
return True
def _reset_problem_attempts_for_student(request, course_id, problem_url, student_identifier):
action_name = 'reset'
update_fcn = _reset_problem_attempts_module_state
return _update_problem_module_state_for_student(request, course_id, problem_url, student_identifier,
update_fcn, action_name)
def _reset_problem_attempts_for_all_students(request, course_id, problem_url):
action_name = 'reset'
update_fcn = _reset_problem_attempts_module_state
return _update_problem_module_state_for_all_students(request, course_id, problem_url,
update_fcn, action_name)
def _delete_problem_module_state(request, module_to_delete, module_descriptor):
'''
delete the state
'''
module_to_delete.delete()
return True
def _delete_problem_state_for_student(request, course_id, problem_url, student_ident):
action_name = 'deleted'
update_fcn = _delete_problem_module_state
return _update_problem_module_state_for_student(request, course_id, problem_url,
update_fcn, action_name)
def _delete_problem_state_for_all_students(request, course_id, problem_url):
action_name = 'deleted'
update_fcn = _delete_problem_module_state
return _update_problem_module_state_for_all_students(request, course_id, problem_url,
update_fcn, action_name)
......@@ -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