Define initial celery task on instructor dash.
Add CourseTaskLog, and make calls to regrading. Add regrading implementation, and hack the marshalling of request.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# -*- 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)
|
||||
|
||||
352
lms/djangoapps/courseware/tasks.py
Normal file
352
lms/djangoapps/courseware/tasks.py
Normal file
@@ -0,0 +1,352 @@
|
||||
|
||||
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('<', '<')
|
||||
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">
|
||||
|
||||
Reference in New Issue
Block a user