Commit 69d59638 by muzaffaryousaf

Refactoring the Staff override functionality.

TNL-898 & TNL-899
parent 9700a80e
...@@ -4,6 +4,7 @@ The Peer Assessment Workflow API exposes all public actions required to complete ...@@ -4,6 +4,7 @@ The Peer Assessment Workflow API exposes all public actions required to complete
the workflow for a given submission. the workflow for a given submission.
""" """
import copy
import logging import logging
from django.utils import timezone from django.utils import timezone
...@@ -12,8 +13,7 @@ from django.db import DatabaseError, IntegrityError, transaction ...@@ -12,8 +13,7 @@ from django.db import DatabaseError, IntegrityError, transaction
from dogapi import dog_stats_api from dogapi import dog_stats_api
from openassessment.assessment.models import ( from openassessment.assessment.models import (
Assessment, AssessmentFeedback, AssessmentPart, Assessment, AssessmentFeedback, AssessmentPart,
InvalidRubricSelection, PeerWorkflow, PeerWorkflowItem, InvalidRubricSelection, PeerWorkflow, PeerWorkflowItem)
AssessmentOverride)
from openassessment.assessment.serializers import ( from openassessment.assessment.serializers import (
AssessmentFeedbackSerializer, RubricSerializer, AssessmentFeedbackSerializer, RubricSerializer,
full_assessment_dict, rubric_from_dict, serialize_assessments, full_assessment_dict, rubric_from_dict, serialize_assessments,
...@@ -308,7 +308,7 @@ def _complete_assessment( ...@@ -308,7 +308,7 @@ def _complete_assessment(
scorer_workflow, scorer_workflow,
overall_feedback, overall_feedback,
num_required_grades, num_required_grades,
scored_at scored_at,
): ):
""" """
Internal function for atomic assessment creation. Creates a peer assessment Internal function for atomic assessment creation. Creates a peer assessment
...@@ -672,6 +672,8 @@ def get_submission_to_assess(submission_uuid, graded_by): ...@@ -672,6 +672,8 @@ def get_submission_to_assess(submission_uuid, graded_by):
u"with submission UUID {}".format(submission_uuid) u"with submission UUID {}".format(submission_uuid)
) )
open_item = workflow.find_active_assessments() open_item = workflow.find_active_assessments()
import pudb;pu.db
peer_submission_uuid = open_item.submission_uuid if open_item else None peer_submission_uuid = open_item.submission_uuid if open_item else None
# If there is an active assessment for this user, get that submission, # If there is an active assessment for this user, get that submission,
# otherwise, get the first assessment for review, otherwise, # otherwise, get the first assessment for review, otherwise,
...@@ -952,7 +954,8 @@ def _log_workflow(submission_uuid, workflow): ...@@ -952,7 +954,8 @@ def _log_workflow(submission_uuid, workflow):
dog_stats_api.increment('openassessment.assessment.peer_workflow.count', tags=tags) dog_stats_api.increment('openassessment.assessment.peer_workflow.count', tags=tags)
def create_overridden_assessment(assessment_id, points, scorer_id, comments=None, scored_at=None): @transaction.commit_on_success
def create_overridden_assessment(assessment_id, overridden_name, overridden_points, scorer_id, overridden_feedback=None, scored_at=None):
""" """
Create a new override assessment. Create a new override assessment.
...@@ -969,15 +972,45 @@ def create_overridden_assessment(assessment_id, points, scorer_id, comments=None ...@@ -969,15 +972,45 @@ def create_overridden_assessment(assessment_id, points, scorer_id, comments=None
AssessmentOverride AssessmentOverride
""" """
try: try:
points = int(points) points = int(overridden_points)
# Get the particular assessment to Override/Regrade # Get the particular assessment to Override/Regrade
assessment = Assessment.objects.get(pk=assessment_id) assessment = Assessment.objects.get(pk=assessment_id)
if points > assessment.points_possible:
raise PeerAssessmentRequestError(u'New grade points must be less than or equal to possible points.')
return AssessmentOverride.create(assessment=assessment, points=points, comments=comments, #
scorer_id=scorer_id, scored_at=scored_at) # if points > assessment.points_possible:
# raise PeerAssessmentRequestError(u'New grade points must be less than or equal to possible points.')
# Create the peer assessment
overridden_assessment = Assessment.create(
assessment.rubric,
scorer_id,
assessment.submission_uuid,
score_type='ST',
scored_at=scored_at,
feedback=assessment.feedback
)
options_selected = {'Idea': 'Good'}
criterion_feedback = {'Idea': overridden_feedback}
overridden_assessment_part = AssessmentPart.create_from_option_names(overridden_assessment, options_selected, feedback=criterion_feedback)
# TODO should we create criteria option with new points????
#
# for part in assessment.parts.all():
# overridden_part = copy.deepcopy(part)
# if part.criterion.name == overridden_name:
# overridden_part.criterion.feedback = overridden_feedback
# overridden_part.option.points = overridden_points
# assessment.overridden_by = overridden_assessment
assessment.save()
# Create assessment parts for each criterion in the rubric
# This will raise an `InvalidRubricSelection` if the selected options do not
# match the rubric.
# AssessmentPart.create_from_option_names(assessment, options_selected, feedback=criterion_feedback)
return overridden_assessment
except ValueError: except ValueError:
raise PeerAssessmentRequestError(u'New grade must be an integer.') raise PeerAssessmentRequestError(u'New grade must be an integer.')
......
...@@ -8,23 +8,15 @@ from django.db import models ...@@ -8,23 +8,15 @@ from django.db import models
class Migration(SchemaMigration): class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Deleting field 'AssessmentOverride.comment' # Adding field 'Assessment.overridden_by'
db.delete_column('assessment_assessmentoverride', 'comment') db.add_column('assessment_assessment', 'overridden_by',
self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['assessment.Assessment'], blank=True),
# Adding field 'AssessmentOverride.comments'
db.add_column('assessment_assessmentoverride', 'comments',
self.gf('django.db.models.fields.TextField')(default='', max_length=10000, blank=True),
keep_default=False) keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Adding field 'AssessmentOverride.comment' # Deleting field 'Assessment.overridden_by'
db.add_column('assessment_assessmentoverride', 'comment', db.delete_column('assessment_assessment', 'overridden_by_id')
self.gf('django.db.models.fields.TextField')(default='', max_length=10000, blank=True),
keep_default=False)
# Deleting field 'AssessmentOverride.comments'
db.delete_column('assessment_assessmentoverride', 'comments')
models = { models = {
...@@ -76,6 +68,7 @@ class Migration(SchemaMigration): ...@@ -76,6 +68,7 @@ class Migration(SchemaMigration):
'Meta': {'ordering': "['-scored_at', '-id']", 'object_name': 'Assessment'}, 'Meta': {'ordering': "['-scored_at', '-id']", 'object_name': 'Assessment'},
'feedback': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '10000', 'blank': 'True'}), 'feedback': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '10000', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'overridden_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['assessment.Assessment']", 'blank': 'True'}),
'rubric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['assessment.Rubric']"}), 'rubric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['assessment.Rubric']"}),
'score_type': ('django.db.models.fields.CharField', [], {'max_length': '2'}), 'score_type': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
'scored_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}), 'scored_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
...@@ -95,15 +88,6 @@ class Migration(SchemaMigration): ...@@ -95,15 +88,6 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'text': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) 'text': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
}, },
'assessment.assessmentoverride': {
'Meta': {'object_name': 'AssessmentOverride'},
'assessment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'override'", 'to': "orm['assessment.Assessment']"}),
'comments': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '10000', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'points': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'scored_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
'scorer_id': ('django.db.models.fields.CharField', [], {'max_length': '40', 'db_index': 'True'})
},
'assessment.assessmentpart': { 'assessment.assessmentpart': {
'Meta': {'object_name': 'AssessmentPart'}, 'Meta': {'object_name': 'AssessmentPart'},
'assessment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'parts'", 'to': "orm['assessment.Assessment']"}), 'assessment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'parts'", 'to': "orm['assessment.Assessment']"}),
......
...@@ -8,22 +8,14 @@ from django.db import models ...@@ -8,22 +8,14 @@ from django.db import models
class Migration(SchemaMigration): class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Adding model 'AssessmentOverride'
db.create_table('assessment_assessmentoverride', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('assessment', self.gf('django.db.models.fields.related.ForeignKey')(related_name='override', to=orm['assessment.Assessment'])),
('scored_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, db_index=True)),
('scorer_id', self.gf('django.db.models.fields.CharField')(max_length=40, db_index=True)),
('points', self.gf('django.db.models.fields.IntegerField')(db_index=True)),
('comment', self.gf('django.db.models.fields.TextField')(default='', max_length=10000, blank=True)),
))
db.send_create_signal('assessment', ['AssessmentOverride'])
# Changing field 'Assessment.overridden_by'
db.alter_column('assessment_assessment', 'overridden_by_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['assessment.Assessment'], null=True))
def backwards(self, orm): def backwards(self, orm):
# Deleting model 'AssessmentOverride'
db.delete_table('assessment_assessmentoverride')
# Changing field 'Assessment.overridden_by'
db.alter_column('assessment_assessment', 'overridden_by_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['assessment.Assessment']))
models = { models = {
'assessment.aiclassifier': { 'assessment.aiclassifier': {
...@@ -74,6 +66,7 @@ class Migration(SchemaMigration): ...@@ -74,6 +66,7 @@ class Migration(SchemaMigration):
'Meta': {'ordering': "['-scored_at', '-id']", 'object_name': 'Assessment'}, 'Meta': {'ordering': "['-scored_at', '-id']", 'object_name': 'Assessment'},
'feedback': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '10000', 'blank': 'True'}), 'feedback': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '10000', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'overridden_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['assessment.Assessment']", 'null': 'True', 'blank': 'True'}),
'rubric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['assessment.Rubric']"}), 'rubric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['assessment.Rubric']"}),
'score_type': ('django.db.models.fields.CharField', [], {'max_length': '2'}), 'score_type': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
'scored_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}), 'scored_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
...@@ -93,15 +86,6 @@ class Migration(SchemaMigration): ...@@ -93,15 +86,6 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'text': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) 'text': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
}, },
'assessment.assessmentoverride': {
'Meta': {'object_name': 'AssessmentOverride'},
'assessment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'override'", 'to': "orm['assessment.Assessment']"}),
'comment': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '10000', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'points': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'scored_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
'scorer_id': ('django.db.models.fields.CharField', [], {'max_length': '40', 'db_index': 'True'})
},
'assessment.assessmentpart': { 'assessment.assessmentpart': {
'Meta': {'object_name': 'AssessmentPart'}, 'Meta': {'object_name': 'AssessmentPart'},
'assessment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'parts'", 'to': "orm['assessment.Assessment']"}), 'assessment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'parts'", 'to': "orm['assessment.Assessment']"}),
......
...@@ -413,6 +413,7 @@ class Assessment(models.Model): ...@@ -413,6 +413,7 @@ class Assessment(models.Model):
submission_uuid = models.CharField(max_length=128, db_index=True) submission_uuid = models.CharField(max_length=128, db_index=True)
rubric = models.ForeignKey(Rubric) rubric = models.ForeignKey(Rubric)
overridden_by = models.ForeignKey('self', blank=True, null=True)
scored_at = models.DateTimeField(default=now, db_index=True) scored_at = models.DateTimeField(default=now, db_index=True)
scorer_id = models.CharField(max_length=40, db_index=True) scorer_id = models.CharField(max_length=40, db_index=True)
...@@ -826,57 +827,3 @@ class AssessmentPart(models.Model): ...@@ -826,57 +827,3 @@ class AssessmentPart(models.Model):
if len(missing_criteria) > 0: if len(missing_criteria) > 0:
msg = u"Missing selections for criteria: {missing}".format(missing=', '.join(missing_criteria)) msg = u"Missing selections for criteria: {missing}".format(missing=', '.join(missing_criteria))
raise InvalidRubricSelection(msg) raise InvalidRubricSelection(msg)
\ No newline at end of file
class AssessmentOverride(models.Model):
"""Override/Regrade an Assessment.
This is an assessment state information and is created when a staff overrides
an assessment of some student.
"""
MAX_COMMENT_SIZE = 1000 * 10
assessment = models.ForeignKey(Assessment, related_name='overridden')
scored_at = models.DateTimeField(default=now, db_index=True)
scorer_id = models.CharField(max_length=40, db_index=True)
points = models.IntegerField(db_index=True)
comments = models.TextField(max_length=10000, default="", blank=True)
class Meta:
app_label = "assessment"
@classmethod
def create(cls, assessment, points, scorer_id, comments=None, scored_at=None):
"""
Create a new override assessment.
Args:
assessment (Assessment): The assessment associated with this override.
scorer_id (unicode): The ID of the staff.
points (int): The points given by staff.
Keyword Arguments:
feedback (unicode): Overall feedback on the submission.
scored_at (datetime): The time the assessment override was created. Defaults to the current time.
Returns:
AssessmentOverride
"""
assessment_params = {
'points': points,
'scorer_id': scorer_id,
'assessment': assessment,
}
if scored_at is not None:
assessment_params['scored_at'] = scored_at
# Truncate the comment if it exceeds the maximum size
if comments is not None:
assessment_params['comments'] = comments[0:cls.MAX_COMMENT_SIZE]
return cls.objects.create(**assessment_params)
...@@ -235,7 +235,7 @@ def full_assessment_dict(assessment, rubric_dict=None): ...@@ -235,7 +235,7 @@ def full_assessment_dict(assessment, rubric_dict=None):
assessment_dict["id"] = assessment.id assessment_dict["id"] = assessment.id
# Get the latest overridden staff grade # Get the latest overridden staff grade
assessment_dict["staff_overridden_grade"] = assessment.latest_overridden_assessment # assessment_dict["staff_overridden_grade"] = assessment.latest_overridden_assessment
cache.set(assessment_cache_key, assessment_dict) cache.set(assessment_cache_key, assessment_dict)
......
...@@ -271,7 +271,8 @@ class GradeMixin(object): ...@@ -271,7 +271,8 @@ class GradeMixin(object):
for assessment in peer_assessments: for assessment in peer_assessments:
for part in assessment['parts']: for part in assessment['parts']:
feedback = None if assessment['staff_overridden_grade'] else part.get('feedback') # feedback = None if assessment['staff_overridden_grade'] else part.get('feedback')
feedback = part.get('feedback')
if feedback: if feedback:
part_criterion_name = part['criterion']['name'] part_criterion_name = part['criterion']['name']
peer_criteria_feedback[part_criterion_name].append(feedback) peer_criteria_feedback[part_criterion_name].append(feedback)
......
...@@ -5,13 +5,10 @@ determine the flow of the problem. ...@@ -5,13 +5,10 @@ determine the flow of the problem.
import copy import copy
from functools import wraps from functools import wraps
import logging import logging
from django.template import Context
from django.template.loader import get_template
from xblock.core import XBlock from xblock.core import XBlock
from openassessment.assessment.errors import PeerAssessmentInternalError, PeerAssessmentRequestError from openassessment.assessment.errors import PeerAssessmentInternalError, PeerAssessmentRequestError
from openassessment.assessment.errors.ai import AIError from openassessment.assessment.errors.ai import AIError
from openassessment.assessment.models import Assessment, AssessmentOverride
from openassessment.xblock.resolve_dates import DISTANT_PAST, DISTANT_FUTURE from openassessment.xblock.resolve_dates import DISTANT_PAST, DISTANT_FUTURE
from openassessment.xblock.data_conversion import ( from openassessment.xblock.data_conversion import (
create_rubric_dict, convert_training_examples_list_to_dict create_rubric_dict, convert_training_examples_list_to_dict
...@@ -39,6 +36,7 @@ def require_global_admin(error_key): ...@@ -39,6 +36,7 @@ def require_global_admin(error_key):
Decorated function Decorated function
""" """
def _decorator(func): # pylint: disable=C0111 def _decorator(func): # pylint: disable=C0111
@wraps(func) @wraps(func)
def _wrapped(xblock, *args, **kwargs): # pylint: disable=C0111 def _wrapped(xblock, *args, **kwargs): # pylint: disable=C0111
...@@ -50,7 +48,9 @@ def require_global_admin(error_key): ...@@ -50,7 +48,9 @@ def require_global_admin(error_key):
return {'success': False, 'msg': permission_errors[error_key]} return {'success': False, 'msg': permission_errors[error_key]}
else: else:
return func(xblock, *args, **kwargs) return func(xblock, *args, **kwargs)
return _wrapped return _wrapped
return _decorator return _decorator
...@@ -67,6 +67,7 @@ def require_course_staff(error_key): ...@@ -67,6 +67,7 @@ def require_course_staff(error_key):
decorated function decorated function
""" """
def _decorator(func): # pylint: disable=C0111 def _decorator(func): # pylint: disable=C0111
@wraps(func) @wraps(func)
def _wrapped(xblock, *args, **kwargs): # pylint: disable=C0111 def _wrapped(xblock, *args, **kwargs): # pylint: disable=C0111
...@@ -79,7 +80,9 @@ def require_course_staff(error_key): ...@@ -79,7 +80,9 @@ def require_course_staff(error_key):
return xblock.render_error(permission_errors[error_key]) return xblock.render_error(permission_errors[error_key])
else: else:
return func(xblock, *args, **kwargs) return func(xblock, *args, **kwargs)
return _wrapped return _wrapped
return _decorator return _decorator
...@@ -286,17 +289,23 @@ class StaffInfoMixin(object): ...@@ -286,17 +289,23 @@ class StaffInfoMixin(object):
@XBlock.json_handler @XBlock.json_handler
@require_course_staff("STAFF_INFO") @require_course_staff("STAFF_INFO")
def staff_override_assessment(self, data, suffix=''): def staff_override_assessment(self, data, suffix=''):
assessment_id = data.get('assessment_id', '') assessment_id = data.get('assessment_id')
points = data.get('points', '') overridden_points = data.get('points')
comments = data.get('comments', '') overridden_feedback = data.get('overridden_feedback')
overridden_name = data.get('overridden_name')
overridden_criterion = data.get('overridden_criterion')
if points in ['', None]: if overridden_points in ['', None]:
return {"success": False, "msg": self._(u'Please enter integer value for new grade.')} return {"success": False, "msg": self._(u'Please enter integer value for new grade.')}
# return self.render_error(self._(u'"override_assessment" required new grade value.')) # return self.render_error(self._(u'"override_assessment" required new grade value.'))
try: try:
overridden_assessment = peer_api.create_overridden_assessment(assessment_id=assessment_id, points=points, comments=comments, overridden_assessment = peer_api.create_overridden_assessment(assessment_id=assessment_id,
scorer_id=self.get_student_item_dict()["student_id"]) overridden_name=overridden_name,
overridden_points=overridden_points,
overridden_feedback=overridden_feedback,
scorer_id=self.get_student_item_dict()[
"student_id"])
path = 'openassessmentblock/staff_debug/staff_regrade_info.html' path = 'openassessmentblock/staff_debug/staff_regrade_info.html'
context_dict = { context_dict = {
......
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