Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-ora2
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-ora2
Commits
db615fab
Commit
db615fab
authored
Mar 17, 2014
by
Stephen Sanchez
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Setting additional metadata for completing a peer workflow
parent
ad1a8f86
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
320 additions
and
198 deletions
+320
-198
apps/openassessment/assessment/migrations/0004_auto__add_field_peerworkflow_completed_at__add_field_peerworkflowitem_.py
+99
-0
apps/openassessment/assessment/models.py
+18
-6
apps/openassessment/assessment/peer_api.py
+53
-56
apps/openassessment/assessment/self_api.py
+8
-22
apps/openassessment/assessment/serializers.py
+36
-60
apps/openassessment/assessment/test/test_peer.py
+70
-4
apps/openassessment/assessment/test/test_self.py
+6
-13
apps/openassessment/management/tests/test_create_oa_submissions.py
+3
-6
apps/openassessment/xblock/grade_mixin.py
+20
-24
apps/openassessment/xblock/self_assessment_mixin.py
+4
-3
apps/openassessment/xblock/test/test_peer.py
+1
-2
apps/openassessment/xblock/test/test_self.py
+2
-2
No files found.
apps/openassessment/assessment/migrations/0004_auto__add_field_peerworkflow_completed_at__add_field_peerworkflowitem_.py
0 → 100644
View file @
db615fab
# -*- 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 field 'PeerWorkflow.completed_at'
db
.
add_column
(
'assessment_peerworkflow'
,
'completed_at'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)(
null
=
True
),
keep_default
=
False
)
# Adding field 'PeerWorkflowItem.scored'
db
.
add_column
(
'assessment_peerworkflowitem'
,
'scored'
,
self
.
gf
(
'django.db.models.fields.BooleanField'
)(
default
=
False
),
keep_default
=
False
)
def
backwards
(
self
,
orm
):
# Deleting field 'PeerWorkflow.completed_at'
db
.
delete_column
(
'assessment_peerworkflow'
,
'completed_at'
)
# Deleting field 'PeerWorkflowItem.scored'
db
.
delete_column
(
'assessment_peerworkflowitem'
,
'scored'
)
models
=
{
'assessment.assessment'
:
{
'Meta'
:
{
'ordering'
:
"['-scored_at', '-id']"
,
'object_name'
:
'Assessment'
},
'feedback'
:
(
'django.db.models.fields.TextField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'10000'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'rubric'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['assessment.Rubric']"
}),
'score_type'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'2'
}),
'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'
}),
'submission_uuid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'128'
,
'db_index'
:
'True'
})
},
'assessment.assessmentfeedback'
:
{
'Meta'
:
{
'object_name'
:
'AssessmentFeedback'
},
'assessments'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'default'
:
'None'
,
'related_name'
:
"'assessment_feedback'"
,
'symmetrical'
:
'False'
,
'to'
:
"orm['assessment.Assessment']"
}),
'feedback'
:
(
'django.db.models.fields.TextField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'10000'
}),
'helpfulness'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'default'
:
'2'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'submission_uuid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'128'
,
'db_index'
:
'True'
})
},
'assessment.assessmentpart'
:
{
'Meta'
:
{
'object_name'
:
'AssessmentPart'
},
'assessment'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'parts'"
,
'to'
:
"orm['assessment.Assessment']"
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'option'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['assessment.CriterionOption']"
})
},
'assessment.criterion'
:
{
'Meta'
:
{
'ordering'
:
"['rubric', 'order_num']"
,
'object_name'
:
'Criterion'
},
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'order_num'
:
(
'django.db.models.fields.PositiveIntegerField'
,
[],
{}),
'prompt'
:
(
'django.db.models.fields.TextField'
,
[],
{
'max_length'
:
'10000'
}),
'rubric'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'criteria'"
,
'to'
:
"orm['assessment.Rubric']"
})
},
'assessment.criterionoption'
:
{
'Meta'
:
{
'ordering'
:
"['criterion', 'order_num']"
,
'object_name'
:
'CriterionOption'
},
'criterion'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'options'"
,
'to'
:
"orm['assessment.Criterion']"
}),
'explanation'
:
(
'django.db.models.fields.TextField'
,
[],
{
'max_length'
:
'10000'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'order_num'
:
(
'django.db.models.fields.PositiveIntegerField'
,
[],
{}),
'points'
:
(
'django.db.models.fields.PositiveIntegerField'
,
[],
{})
},
'assessment.peerworkflow'
:
{
'Meta'
:
{
'ordering'
:
"['created_at', 'id']"
,
'object_name'
:
'PeerWorkflow'
},
'completed_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'null'
:
'True'
}),
'course_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'40'
,
'db_index'
:
'True'
}),
'created_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'item_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'128'
,
'db_index'
:
'True'
}),
'student_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'40'
,
'db_index'
:
'True'
}),
'submission_uuid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'128'
,
'db_index'
:
'True'
})
},
'assessment.peerworkflowitem'
:
{
'Meta'
:
{
'ordering'
:
"['started_at', 'id']"
,
'object_name'
:
'PeerWorkflowItem'
},
'assessment'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'default'
:
'-1'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'scored'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'scorer_id'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'items'"
,
'to'
:
"orm['assessment.PeerWorkflow']"
}),
'started_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
,
'db_index'
:
'True'
}),
'submission_uuid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'128'
,
'db_index'
:
'True'
})
},
'assessment.rubric'
:
{
'Meta'
:
{
'object_name'
:
'Rubric'
},
'content_hash'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'40'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
})
}
}
complete_apps
=
[
'assessment'
]
\ No newline at end of file
apps/openassessment/assessment/models.py
View file @
db615fab
...
...
@@ -307,7 +307,7 @@ class Assessment(models.Model):
return
median_score
@classmethod
def
scores_by_criterion
(
cls
,
submission_uuid
,
must_be_graded_by
):
def
scores_by_criterion
(
cls
,
assessments
):
"""Create a dictionary of lists for scores associated with criterion
Create a key value in a dict with a list of values, for every criterion
...
...
@@ -318,18 +318,17 @@ class Assessment(models.Model):
of scores.
Args:
submission_uuid (str): Obtain assessments associated with this submission.
must_be_graded_by (int): The number of assessments to include in this score analysis
.
assessments (list): List of assessments to sort scores by their
associated criteria
.
Examples:
>>> Assessment.scores_by_criterion('abcd', 3)
>>> assessments = Assessment.objects.all()
>>> Assessment.scores_by_criterion(assessments)
{
"foo": [1, 2, 3],
"bar": [6, 7, 8]
}
"""
assessments
=
cls
.
objects
.
filter
(
submission_uuid
=
submission_uuid
)
.
order_by
(
"scored_at"
)[:
must_be_graded_by
]
scores
=
defaultdict
(
list
)
for
assessment
in
assessments
:
for
part
in
assessment
.
parts
.
all
():
...
...
@@ -401,6 +400,7 @@ class PeerWorkflow(models.Model):
course_id
=
models
.
CharField
(
max_length
=
40
,
db_index
=
True
)
submission_uuid
=
models
.
CharField
(
max_length
=
128
,
db_index
=
True
,
unique
=
True
)
created_at
=
models
.
DateTimeField
(
default
=
now
,
db_index
=
True
)
completed_at
=
models
.
DateTimeField
(
null
=
True
)
class
Meta
:
ordering
=
[
"created_at"
,
"id"
]
...
...
@@ -433,6 +433,18 @@ class PeerWorkflowItem(models.Model):
started_at
=
models
.
DateTimeField
(
default
=
now
,
db_index
=
True
)
assessment
=
models
.
IntegerField
(
default
=-
1
)
# This WorkflowItem was used to determine the final score for the Workflow.
scored
=
models
.
BooleanField
(
default
=
False
)
@classmethod
def
get_scored_assessments
(
cls
,
submission_uuid
):
workflow_items
=
PeerWorkflowItem
.
objects
.
filter
(
submission_uuid
=
submission_uuid
,
scored
=
True
)
return
Assessment
.
objects
.
filter
(
pk__in
=
[
item
.
pk
for
item
in
workflow_items
]
)
class
Meta
:
ordering
=
[
"started_at"
,
"id"
]
...
...
apps/openassessment/assessment/peer_api.py
View file @
db615fab
...
...
@@ -6,16 +6,20 @@ the workflow for a given submission.
"""
import
copy
import
logging
from
datetime
import
datetime
,
timedelta
from
datetime
import
timedelta
from
django.utils
import
timezone
from
django.utils.translation
import
ugettext
as
_
from
django.db
import
DatabaseError
from
django.db.models
import
Q
from
pytz
import
UTC
from
openassessment.assessment.models
import
Assessment
,
InvalidOptionSelection
,
PeerWorkflow
,
PeerWorkflowItem
,
AssessmentFeedback
from
openassessment.assessment.models
import
(
Assessment
,
InvalidOptionSelection
,
PeerWorkflow
,
PeerWorkflowItem
,
AssessmentFeedback
)
from
openassessment.assessment.serializers
import
(
AssessmentSerializer
,
rubric_from_dict
,
get_assessment_review
,
AssessmentFeedbackSerializer
)
AssessmentSerializer
,
rubric_from_dict
,
AssessmentFeedbackSerializer
,
full_assessment_dict
)
from
submissions.api
import
get_submission_and_student
from
submissions.models
import
Submission
,
StudentItem
from
submissions.serializers
import
SubmissionSerializer
,
StudentItemSerializer
...
...
@@ -93,17 +97,25 @@ def get_score(submission_uuid, requirements):
if
not
is_complete
(
submission_uuid
,
requirements
):
return
None
assessments
=
Assessment
.
objects
.
filter
(
submission_uuid
=
submission_uuid
,
score_type
=
PEER_TYPE
)
submission_finished
=
_check_submission_graded
(
submission_uuid
,
requirements
[
"must_be_graded_by"
])
assessments
=
Assessment
.
objects
.
filter
(
submission_uuid
=
submission_uuid
,
score_type
=
PEER_TYPE
)[:
requirements
[
"must_be_graded_by"
]]
submission_finished
=
assessments
.
count
()
>=
requirements
[
"must_be_graded_by"
]
if
not
submission_finished
:
return
None
PeerWorkflowItem
.
objects
.
filter
(
assessment__in
=
[
a
.
pk
for
a
in
assessments
]
)
.
update
(
scored
=
True
)
PeerWorkflow
.
objects
.
filter
(
submission_uuid
=
submission_uuid
)
.
update
(
completed_at
=
timezone
.
now
()
)
return
{
"points_earned"
:
sum
(
get_assessment_median_scores
(
submission_uuid
,
requirements
[
"must_be_graded_by"
]
)
.
values
()
get_assessment_median_scores
(
submission_uuid
)
.
values
()
),
"points_possible"
:
assessments
[
0
]
.
points_possible
,
}
...
...
@@ -255,7 +267,7 @@ def get_rubric_max_scores(submission_uuid):
raise
PeerAssessmentInternalError
(
error_message
)
def
get_assessment_median_scores
(
submission_uuid
,
must_be_graded_by
):
def
get_assessment_median_scores
(
submission_uuid
):
"""Get the median score for each rubric criterion
For a given assessment, collect the median score for each criterion on the
...
...
@@ -266,14 +278,10 @@ def get_assessment_median_scores(submission_uuid, must_be_graded_by):
values, the average of those two values is returned, rounded up to the
greatest integer value.
If OverGrading occurs, the 'must_be_graded_by' parameter is the number of
assessments we want to use to calculate the median values. If this limit is
less than the total number of assessments available, the earliest
assessments are used.
Args:
submission_uuid (str): The submission uuid to get all rubric criterion median scores.
must_be_graded_by (int): The number of assessments to include in this score analysis.
submission_uuid (str): The submission uuid is used to get the
assessments used to score this submission, and generate the
appropriate median score.
Returns:
(dict): A dictionary of rubric criterion names, with a median score of
...
...
@@ -283,10 +291,9 @@ def get_assessment_median_scores(submission_uuid, must_be_graded_by):
PeerAssessmentInternalError: If any error occurs while retrieving
information to form the median scores, an error is raised.
"""
# Create a key value in a dict with a list of values, for every criterion
# found in an assessment.
try
:
scores
=
Assessment
.
scores_by_criterion
(
submission_uuid
,
must_be_graded_by
)
assessments
=
PeerWorkflowItem
.
get_scored_assessments
(
submission_uuid
)
scores
=
Assessment
.
scores_by_criterion
(
assessments
)
return
Assessment
.
get_median_score_dict
(
scores
)
except
DatabaseError
:
error_message
=
_
(
u"Error getting assessment median scores {}"
.
format
(
submission_uuid
))
...
...
@@ -341,7 +348,7 @@ def has_finished_required_evaluating(student_item_dict, required_assessments):
return
done
,
count
def
get_assessments
(
submission_uuid
):
def
get_assessments
(
submission_uuid
,
scored_only
=
True
,
limit
=
None
):
"""Retrieve the assessments for a submission.
Retrieves all the assessments for a submissions. This API returns related
...
...
@@ -349,8 +356,13 @@ def get_assessments(submission_uuid):
assessments associated with this submission will not be returned.
Args:
submission_uuid (str): The UUID of the submission all the
requested assessments are associated with.
submission_uuid (str): The submission all the requested assessments are
associated with. Required.
Kwargs:
scored (boolean): Only retrieve the assessments used to generate a score
for this submission.
limit (int): Limit the returned assessments. If None, returns all.
Returns:
list(dict): A list of dictionaries, where each dictionary represents a
...
...
@@ -363,7 +375,7 @@ def get_assessments(submission_uuid):
while retrieving the assessments associated with this submission.
Examples:
>>> get_assessments("1")
>>> get_assessments("1"
, scored_only=True, limit=2
)
[
{
'points_earned': 6,
...
...
@@ -383,7 +395,16 @@ def get_assessments(submission_uuid):
"""
try
:
return
get_assessment_review
(
submission_uuid
)
if
scored_only
:
assessments
=
PeerWorkflowItem
.
get_scored_assessments
(
submission_uuid
)
else
:
assessments
=
Assessment
.
objects
.
filter
(
submission_uuid
=
submission_uuid
,
score_type
=
PEER_TYPE
)
return
[
full_assessment_dict
(
assessment
)
for
assessment
in
assessments
[:
limit
]]
except
DatabaseError
:
error_message
=
_
(
u"Error getting assessments for submission {}"
.
format
(
submission_uuid
)
...
...
@@ -672,7 +693,7 @@ def _find_active_assessments(workflow):
"""
workflows
=
workflow
.
items
.
filter
(
assessment
=-
1
,
started_at__gt
=
datetime
.
utcnow
()
.
replace
(
tzinfo
=
UTC
)
-
TIME_LIMIT
started_at__gt
=
timezone
.
now
(
)
-
TIME_LIMIT
)
return
workflows
[
0
]
.
submission_uuid
if
workflows
else
None
...
...
@@ -709,7 +730,7 @@ def _get_submission_for_review(workflow, graded_by, over_grading=False):
"""
order
=
" having count(pwi.id) <
%
s order by pw.created_at, pw.id "
timeout
=
(
datetime
.
utcnow
()
.
replace
(
tzinfo
=
UTC
)
-
TIME_LIMIT
)
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
)
timeout
=
(
timezone
.
now
(
)
-
TIME_LIMIT
)
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
)
sub
=
_get_next_submission
(
order
,
workflow
,
...
...
@@ -738,7 +759,7 @@ def _get_submission_for_over_grading(workflow):
"""
order
=
" order by c, pw.created_at, pw.id "
timeout
=
(
datetime
.
utcnow
()
.
replace
(
tzinfo
=
UTC
)
-
TIME_LIMIT
)
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
)
timeout
=
(
timezone
.
now
(
)
-
TIME_LIMIT
)
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
)
return
_get_next_submission
(
order
,
workflow
,
...
...
@@ -826,7 +847,7 @@ def _get_next_submission(order, workflow, *args):
def
_assessors_count
(
peer_workflow
):
return
PeerWorkflowItem
.
objects
.
filter
(
~
Q
(
assessment
=-
1
)
|
Q
(
assessment
=-
1
,
started_at__gt
=
datetime
.
utcnow
()
.
replace
(
tzinfo
=
UTC
)
-
TIME_LIMIT
),
Q
(
assessment
=-
1
,
started_at__gt
=
timezone
.
now
(
)
-
TIME_LIMIT
),
submission_uuid
=
peer_workflow
.
submission_uuid
)
.
count
()
...
...
@@ -897,28 +918,6 @@ def _check_student_done_grading(workflow, must_grade):
return
workflow
.
items
.
all
()
.
exclude
(
assessment
=-
1
)
.
count
()
>=
must_grade
def
_check_submission_graded
(
submission_uuid
,
must_be_graded_by
):
"""Checks to see if the submission has enough grades.
Determine if the given submission has enough grades.
Args:
submission_uuid (str): The submission we want to check to see if it has
been graded by enough peers.
must_be_graded_by (int): The number of peer assessments there should be.
Returns:
True if the submission is finished, False if not.
Examples:
>>> _check_submission_graded("1", 3)
True
"""
return
PeerWorkflowItem
.
objects
.
filter
(
submission_uuid
=
submission_uuid
)
.
exclude
(
assessment
=-
1
)
.
count
()
>=
must_be_graded_by
def
get_assessment_feedback
(
submission_uuid
):
"""Retrieve a feedback object for an assessment whether it exists or not.
...
...
@@ -950,15 +949,13 @@ def get_assessment_feedback(submission_uuid):
raise
PeerAssessmentInternalError
(
error_message
)
def
set_assessment_feedback
(
must_grade
,
feedback_dict
):
def
set_assessment_feedback
(
feedback_dict
):
"""Set a feedback object for an assessment to have some new values.
Sets or updates the assessment feedback with the given values in the
dict.
Args:
must_grade (int): The required number of assessments for the associated
submission.
feedback_dict (dict): A dictionary of all the values to update or create
a new assessment feedback.
Returns:
...
...
@@ -970,7 +967,7 @@ def set_assessment_feedback(must_grade, feedback_dict):
logger
.
error
(
error_message
)
raise
PeerAssessmentRequestError
(
error_message
)
try
:
assessments
=
Assessment
.
objects
.
filter
(
submission__uuid
=
submission_uuid
,
score_type
=
"PE"
)
assessments
=
PeerWorkflowItem
.
get_scored_assessments
(
submission_uuid
)
except
DatabaseError
:
error_message
=
(
u"An error occurred getting database state to set assessment feedback for {}."
...
...
@@ -987,7 +984,7 @@ def set_assessment_feedback(must_grade, feedback_dict):
# Assessments associated with feedback must be saved after the row is
# committed to the database in order to associated the PKs across both
# tables.
feedback_model
.
assessments
.
add
(
*
[
assessment
.
id
for
assessment
in
assessments
[:
must_grade
]]
)
feedback_model
.
assessments
.
add
(
*
assessments
)
except
DatabaseError
:
error_message
=
(
u"An error occurred saving assessment feedback for {}."
...
...
apps/openassessment/assessment/self_api.py
View file @
db615fab
...
...
@@ -92,35 +92,22 @@ def create_assessment(submission_uuid, user_id, options_selected, rubric_dict, s
return
serializer
.
data
def
get_
submission_and_
assessment
(
submission_uuid
):
def
get_assessment
(
submission_uuid
):
"""
Retrieve a s
ubmission and self-assessment for a student item
.
Retrieve a s
elf-assessment for a submission_uuid
.
Args:
submission_uuid (str): The submission
uuid
for we want information for
submission_uuid (str): The submission
UUID
for we want information for
regarding self assessment.
Returns:
A tuple `(submission, assessment)` where:
submission (dict) is a serialized Submission model, or None (if the user has not yet made a submission)
assessment (dict) is a serialized Assessment model, or None (if the user has not yet self-assessed)
assessment (dict) is a serialized Assessment model, or None (if the user has not yet self-assessed)
If multiple submissions or self-assessments are found, returns the most recent one.
Raises:
SelfAssessmentRequestError:
Student item dict
was invalid.
SelfAssessmentRequestError:
submission_uuid
was invalid.
"""
# Look up the most recent submission from the student item
try
:
submission
=
get_submission
(
submission_uuid
)
if
not
submission
:
return
(
None
,
None
)
except
SubmissionNotFoundError
:
return
(
None
,
None
)
except
SubmissionRequestError
:
raise
SelfAssessmentRequestError
(
_
(
'Could not retrieve submission'
))
# Retrieve assessments for the submission
# Retrieve assessments for the submission UUID
# We weakly enforce that number of self-assessments per submission is <= 1,
# but not at the database level. Someone could take advantage of the race condition
# between checking the number of self-assessments and creating a new self-assessment.
...
...
@@ -131,9 +118,8 @@ def get_submission_and_assessment(submission_uuid):
if
assessments
.
exists
():
assessment_dict
=
full_assessment_dict
(
assessments
[
0
])
return
submission
,
assessment_dict
else
:
return
submission
,
None
return
assessment_dict
return
None
def
is_complete
(
submission_uuid
):
...
...
apps/openassessment/assessment/serializers.py
View file @
db615fab
...
...
@@ -8,8 +8,8 @@ from copy import deepcopy
from
django.utils.translation
import
ugettext
as
_
from
rest_framework
import
serializers
from
openassessment.assessment.models
import
(
Assessment
,
AssessmentFeedback
,
AssessmentPart
,
Criterion
,
CriterionOption
,
Rubric
)
Assessment
,
AssessmentFeedback
,
AssessmentPart
,
Criterion
,
CriterionOption
,
Rubric
,
PeerWorkflowItem
,
PeerWorkflow
)
class
InvalidRubric
(
Exception
):
...
...
@@ -132,61 +132,6 @@ class AssessmentSerializer(serializers.ModelSerializer):
)
def
get_assessment_review
(
submission_uuid
):
"""Get all information pertaining to an assessment for review.
Given an assessment serializer, return a serializable formatted model of
the assessment, all assessment parts, all criterion options, and the
associated rubric.
Args:
submission_uuid (str): The UUID of the submission whose assessment reviews we want to retrieve.
Returns:
(list): A list of assessment reviews, combining assessments with
rubrics and assessment parts, to allow a cohesive object for
rendering the complete peer grading workflow.
Examples:
>>> get_assessment_review(submission, score_type)
[{
'submission': 1,
'rubric': {
'id': 1,
'content_hash': u'45cc932c4da12a1c2b929018cd6f0785c1f8bc07',
'criteria': [{
'order_num': 0,
'name': u'Spelling',
'prompt': u'Did the student have spelling errors?',
'options': [{
'order_num': 0,
'points': 2,
'name': u'No spelling errors',
'explanation': u'No spelling errors were found in this submission.',
}]
}]
},
'scored_at': datetime.datetime(2014, 2, 25, 19, 50, 7, 290464, tzinfo=<UTC>),
'scorer_id': u'Bob',
'score_type': u'PE',
'parts': [{
'option': {
'order_num': 0,
'points': 2,
'name': u'No spelling errors',
'explanation': u'No spelling errors were found in this submission.'}
}],
'submission_uuid': u'0a600160-be7f-429d-a853-1283d49205e7',
'points_earned': 9,
'points_possible': 20,
}]
"""
return
[
full_assessment_dict
(
assessment
)
for
assessment
in
Assessment
.
objects
.
filter
(
submission_uuid
=
submission_uuid
)
]
def
full_assessment_dict
(
assessment
):
"""
Return a dict representation of the Assessment model,
...
...
@@ -278,10 +223,41 @@ class AssessmentFeedbackSerializer(serializers.ModelSerializer):
class
Meta
:
model
=
AssessmentFeedback
fields
=
(
'submission_uuid'
,
'helpfulness'
,
'feedback'
,
'assessments'
,)
class
PeerWorkflowSerializer
(
serializers
.
ModelSerializer
):
"""Representation of the PeerWorkflow.
A PeerWorkflow should not be exposed to the front end of any question. This
model should only be exposed externally for administrative views, in order
to visualize the Peer Workflow.
"""
class
Meta
:
model
=
PeerWorkflow
fields
=
(
'student_id'
,
'item_id'
,
'course_id'
,
'submission_uuid'
,
'helpfulness'
,
'feedback'
,
'assessments'
,
'created_at'
,
'completed_at'
)
class
PeerWorkflowItemSerializer
(
serializers
.
ModelSerializer
):
"""Representation of the PeerWorkflowItem
As with the PeerWorkflow, this should not be exposed to the front end. This
should only be used to visualize the Peer Workflow in an administrative
view.
"""
class
Meta
:
model
=
PeerWorkflowItem
fields
=
(
'scorer_id'
,
'submission_uuid'
,
'started_at'
,
'assessment'
,
'scored'
)
apps/openassessment/assessment/test/test_peer.py
View file @
db615fab
...
...
@@ -10,7 +10,7 @@ from mock import patch
from
nose.tools
import
raises
from
openassessment.assessment
import
peer_api
from
openassessment.assessment.models
import
Assessment
,
PeerWorkflow
,
PeerWorkflowItem
from
openassessment.assessment.models
import
Assessment
,
PeerWorkflow
,
PeerWorkflowItem
,
AssessmentFeedback
from
openassessment.workflow
import
api
as
workflow_api
from
submissions
import
api
as
sub_api
from
submissions.models
import
Submission
...
...
@@ -136,7 +136,7 @@ class TestPeerApi(TestCase):
assessment_dict
,
RUBRIC_DICT
,
)
assessments
=
peer_api
.
get_assessments
(
sub
[
"uuid"
])
assessments
=
peer_api
.
get_assessments
(
sub
[
"uuid"
]
,
scored_only
=
False
)
self
.
assertEqual
(
1
,
len
(
assessments
))
@file_data
(
'valid_assessments.json'
)
...
...
@@ -151,7 +151,7 @@ class TestPeerApi(TestCase):
RUBRIC_DICT
,
MONDAY
)
assessments
=
peer_api
.
get_assessments
(
sub
[
"uuid"
])
assessments
=
peer_api
.
get_assessments
(
sub
[
"uuid"
]
,
scored_only
=
False
)
self
.
assertEqual
(
1
,
len
(
assessments
))
self
.
assertEqual
(
assessments
[
0
][
"scored_at"
],
MONDAY
)
...
...
@@ -480,6 +480,45 @@ class TestPeerApi(TestCase):
submission_uuid
=
peer_api
.
_get_submission_for_over_grading
(
xander_workflow
)
self
.
assertEqual
(
buffy_answer
[
"uuid"
],
submission_uuid
)
def
test_create_assessment_feedback
(
self
):
tim_sub
,
tim
=
self
.
_create_student_and_submission
(
"Tim"
,
"Tim's answer"
)
bob_sub
,
bob
=
self
.
_create_student_and_submission
(
"Bob"
,
"Bob's answer"
)
sub
=
peer_api
.
get_submission_to_assess
(
bob
,
1
)
assessment
=
peer_api
.
create_assessment
(
sub
[
"uuid"
],
bob
[
"student_id"
],
ASSESSMENT_DICT
,
RUBRIC_DICT
,
)
sub
=
peer_api
.
get_submission_to_assess
(
tim
,
1
)
peer_api
.
create_assessment
(
sub
[
"uuid"
],
tim
[
"student_id"
],
ASSESSMENT_DICT
,
RUBRIC_DICT
,
)
peer_api
.
get_score
(
tim_sub
[
"uuid"
],
{
'must_grade'
:
1
,
'must_be_graded_by'
:
1
}
)
feedback
=
peer_api
.
get_assessment_feedback
(
tim_sub
[
'uuid'
])
self
.
assertIsNone
(
feedback
)
feedback
=
peer_api
.
set_assessment_feedback
(
{
'submission_uuid'
:
tim_sub
[
'uuid'
],
'helpfulness'
:
0
,
'feedback'
:
'Bob is a jerk!'
}
)
self
.
assertIsNotNone
(
feedback
)
self
.
assertEquals
(
feedback
[
"assessments"
][
0
][
"submission_uuid"
],
assessment
[
"submission_uuid"
])
saved_feedback
=
peer_api
.
get_assessment_feedback
(
tim_sub
[
'uuid'
])
self
.
assertEquals
(
feedback
,
saved_feedback
)
def
test_close_active_assessment
(
self
):
buffy_answer
,
buffy
=
self
.
_create_student_and_submission
(
"Buffy"
,
"Buffy's answer"
)
xander_answer
,
xander
=
self
.
_create_student_and_submission
(
"Xander"
,
"Xander's answer"
)
...
...
@@ -512,6 +551,33 @@ class TestPeerApi(TestCase):
mock_filter
.
side_effect
=
DatabaseError
(
"Oh no."
)
peer_api
.
_get_submission_for_review
(
tim_workflow
,
3
)
@patch.object
(
AssessmentFeedback
.
objects
,
'get'
)
@raises
(
peer_api
.
PeerAssessmentInternalError
)
def
test_get_assessment_feedback_error
(
self
,
mock_filter
):
mock_filter
.
side_effect
=
DatabaseError
(
"Oh no."
)
tim_answer
,
tim
=
self
.
_create_student_and_submission
(
"Tim"
,
"Tim's answer"
,
MONDAY
)
peer_api
.
get_assessment_feedback
(
tim_answer
[
'uuid'
])
@patch.object
(
PeerWorkflowItem
,
'get_scored_assessments'
)
@raises
(
peer_api
.
PeerAssessmentInternalError
)
def
test_set_assessment_feedback_error
(
self
,
mock_filter
):
mock_filter
.
side_effect
=
DatabaseError
(
"Oh no."
)
tim_answer
,
tim
=
self
.
_create_student_and_submission
(
"Tim"
,
"Tim's answer"
,
MONDAY
)
peer_api
.
set_assessment_feedback
({
'submission_uuid'
:
tim_answer
[
'uuid'
]})
@patch.object
(
AssessmentFeedback
,
'save'
)
@raises
(
peer_api
.
PeerAssessmentInternalError
)
def
test_set_assessment_feedback_error_on_save
(
self
,
mock_filter
):
mock_filter
.
side_effect
=
DatabaseError
(
"Oh no."
)
tim_answer
,
tim
=
self
.
_create_student_and_submission
(
"Tim"
,
"Tim's answer"
,
MONDAY
)
peer_api
.
set_assessment_feedback
(
{
'submission_uuid'
:
tim_answer
[
'uuid'
],
'helpfulness'
:
0
,
'feedback'
:
'Boo'
,
}
)
@patch.object
(
PeerWorkflow
.
objects
,
'filter'
)
@raises
(
peer_api
.
PeerAssessmentWorkflowError
)
def
test_failure_to_get_latest_workflow
(
self
,
mock_filter
):
...
...
@@ -581,7 +647,7 @@ class TestPeerApi(TestCase):
def
test_median_score_db_error
(
self
,
mock_filter
):
mock_filter
.
side_effect
=
DatabaseError
(
"Bad things happened"
)
tim
,
_
=
self
.
_create_student_and_submission
(
"Tim"
,
"Tim's answer"
)
peer_api
.
get_assessment_median_scores
(
tim
[
"uuid"
]
,
3
)
peer_api
.
get_assessment_median_scores
(
tim
[
"uuid"
])
@patch.object
(
Assessment
.
objects
,
'filter'
)
@raises
(
peer_api
.
PeerAssessmentInternalError
)
...
...
apps/openassessment/assessment/test/test_self.py
View file @
db615fab
...
...
@@ -9,8 +9,7 @@ import pytz
from
django.test
import
TestCase
from
submissions.api
import
create_submission
from
openassessment.assessment.self_api
import
(
create_assessment
,
get_submission_and_assessment
,
is_complete
,
SelfAssessmentRequestError
create_assessment
,
is_complete
,
SelfAssessmentRequestError
,
get_assessment
)
...
...
@@ -53,16 +52,13 @@ class TestSelfApi(TestCase):
def
test_create_assessment
(
self
):
# Initially, there should be no submission or self assessment
self
.
assertEqual
(
get_
submission_and_assessment
(
"5"
),
(
None
,
None
)
)
self
.
assertEqual
(
get_
assessment
(
"5"
),
None
)
# Create a submission to self-assess
submission
=
create_submission
(
self
.
STUDENT_ITEM
,
"Test answer"
)
# Now there should be a submission, but no self-assessment
received_submission
,
assessment
=
get_submission_and_assessment
(
submission
[
"uuid"
]
)
self
.
assertItemsEqual
(
received_submission
,
submission
)
assessment
=
get_assessment
(
submission
[
"uuid"
])
self
.
assertIs
(
assessment
,
None
)
self
.
assertFalse
(
is_complete
(
submission
[
'uuid'
]))
...
...
@@ -77,10 +73,7 @@ class TestSelfApi(TestCase):
self
.
assertTrue
(
is_complete
(
submission
[
'uuid'
]))
# Retrieve the self-assessment
received_submission
,
retrieved
=
get_submission_and_assessment
(
submission
[
"uuid"
]
)
self
.
assertItemsEqual
(
received_submission
,
submission
)
retrieved
=
get_assessment
(
submission
[
"uuid"
])
# Check that the assessment we created matches the assessment we retrieved
# and that both have the correct values
...
...
@@ -175,7 +168,7 @@ class TestSelfApi(TestCase):
)
# Retrieve the self-assessment
_
,
retrieved
=
get_submission_and
_assessment
(
submission
[
"uuid"
])
retrieved
=
get
_assessment
(
submission
[
"uuid"
])
# Expect that both the created and retrieved assessments have the same
# timestamp, and it's >= our recorded time.
...
...
@@ -200,7 +193,7 @@ class TestSelfApi(TestCase):
)
# Expect that we still have the original assessment
_
,
retrieved
=
get_submission_and
_assessment
(
submission
[
"uuid"
])
retrieved
=
get
_assessment
(
submission
[
"uuid"
])
self
.
assertItemsEqual
(
assessment
,
retrieved
)
def
test_is_complete_no_submission
(
self
):
...
...
apps/openassessment/management/tests/test_create_oa_submissions.py
View file @
db615fab
...
...
@@ -29,20 +29,17 @@ class CreateSubmissionsTest(TestCase):
self
.
assertGreater
(
len
(
submissions
[
0
][
'answer'
]),
0
)
# Check that peer and self assessments were created
assessments
=
peer_api
.
get_assessments
(
submissions
[
0
][
'uuid'
])
assessments
=
peer_api
.
get_assessments
(
submissions
[
0
][
'uuid'
]
,
scored_only
=
False
)
# Verify that the assessments exist and have content
# TODO: currently peer_api.get_assessments() returns both peer- and self-assessments
# When this call gets split, we'll need to update the test
self
.
assertEqual
(
len
(
assessments
),
cmd
.
NUM_PEER_ASSESSMENTS
+
1
)
self
.
assertEqual
(
len
(
assessments
),
cmd
.
NUM_PEER_ASSESSMENTS
)
for
assessment
in
assessments
:
self
.
assertGreater
(
assessment
[
'points_possible'
],
0
)
# Check that a self-assessment was created
submission
,
assessment
=
self_api
.
get_submission_and
_assessment
(
submissions
[
0
][
'uuid'
])
assessment
=
self_api
.
get
_assessment
(
submissions
[
0
][
'uuid'
])
# Verify that the assessment exists and has content
self
.
assertIsNot
(
submission
,
None
)
self
.
assertIsNot
(
assessment
,
None
)
self
.
assertGreater
(
assessment
[
'points_possible'
],
0
)
apps/openassessment/xblock/grade_mixin.py
View file @
db615fab
...
...
@@ -4,6 +4,8 @@ from django.utils.translation import ugettext as _
from
xblock.core
import
XBlock
from
openassessment.assessment
import
peer_api
from
openassessment.assessment
import
self_api
from
submissions
import
api
as
submission_api
class
GradeMixin
(
object
):
...
...
@@ -23,28 +25,24 @@ class GradeMixin(object):
status
=
workflow
.
get
(
'status'
)
context
=
{}
if
status
==
"done"
:
feedback
=
peer_api
.
get_assessment_feedback
(
self
.
submission_uuid
)
feedback_text
=
feedback
.
get
(
'feedback'
,
''
)
if
feedback
else
''
max_scores
=
peer_api
.
get_rubric_max_scores
(
self
.
submission_uuid
)
path
=
'openassessmentblock/grade/oa_grade_complete.html'
assessment_ui_model
=
self
.
get_assessment_module
(
'peer-assessment'
)
student_submission
=
self
.
get_user_submission
(
workflow
[
"submission_uuid"
]
)
student_score
=
workflow
[
"score"
]
assessments
=
peer_api
.
get_assessments
(
student_submission
[
"uuid"
])
peer_assessments
=
[]
self_assessment
=
None
for
assessment
in
assessments
:
if
assessment
[
"score_type"
]
==
"PE"
:
peer_assessments
.
append
(
assessment
)
else
:
self_assessment
=
assessment
peer_assessments
=
peer_assessments
[:
assessment_ui_model
[
"must_grade"
]]
median_scores
=
peer_api
.
get_assessment_median_scores
(
student_submission
[
"uuid"
],
assessment_ui_model
[
"must_be_graded_by"
]
)
try
:
feedback
=
peer_api
.
get_assessment_feedback
(
self
.
submission_uuid
)
feedback_text
=
feedback
.
get
(
'feedback'
,
''
)
if
feedback
else
''
max_scores
=
peer_api
.
get_rubric_max_scores
(
self
.
submission_uuid
)
path
=
'openassessmentblock/grade/oa_grade_complete.html'
student_submission
=
submission_api
.
get_submission
(
workflow
[
"submission_uuid"
])
student_score
=
workflow
[
"score"
]
peer_assessments
=
peer_api
.
get_assessments
(
student_submission
[
"uuid"
])
self_assessment
=
self_api
.
get_assessment
(
student_submission
[
"uuid"
])
median_scores
=
peer_api
.
get_assessment_median_scores
(
student_submission
[
"uuid"
]
)
except
(
submission_api
.
SubmissionError
,
peer_api
.
PeerAssessmentError
,
self_api
.
SelfAssessmentRequestError
):
return
self
.
render_error
(
_
(
u"An unexpected error occurred."
))
context
[
"feedback_text"
]
=
feedback_text
context
[
"student_submission"
]
=
student_submission
context
[
"peer_assessments"
]
=
peer_assessments
...
...
@@ -75,7 +73,6 @@ class GradeMixin(object):
@XBlock.json_handler
def
feedback_submit
(
self
,
data
,
suffix
=
''
):
"""Attach the Assessment Feedback text to some submission."""
assessment_ui_model
=
self
.
get_assessment_module
(
'peer-assessment'
)
or
{}
assessment_feedback
=
data
.
get
(
'feedback'
,
''
)
if
not
assessment_feedback
:
return
{
...
...
@@ -84,7 +81,6 @@ class GradeMixin(object):
}
try
:
peer_api
.
set_assessment_feedback
(
assessment_ui_model
[
'must_grade'
],
{
'submission_uuid'
:
self
.
submission_uuid
,
'feedback'
:
assessment_feedback
,
...
...
apps/openassessment/xblock/self_assessment_mixin.py
View file @
db615fab
...
...
@@ -3,7 +3,7 @@ from django.utils.translation import ugettext as _
from
xblock.core
import
XBlock
from
openassessment.assessment
import
self_api
from
submissions
import
api
as
submission_api
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -29,10 +29,11 @@ class SelfAssessmentMixin(object):
if
not
workflow
:
return
self
.
render_assessment
(
path
,
context
)
try
:
submission
,
assessment
=
self_api
.
get_submission_and_assessment
(
submission
=
submission_api
.
get_submission
(
self
.
submission_uuid
)
assessment
=
self_api
.
get_assessment
(
workflow
[
"submission_uuid"
]
)
except
self_api
.
SelfAssessmentRequestError
:
except
(
submission_api
.
SubmissionError
,
self_api
.
SelfAssessmentRequestError
)
:
logger
.
exception
(
u"Could not retrieve self assessment for submission {}"
.
format
(
workflow
[
"submission_uuid"
])
...
...
apps/openassessment/xblock/test/test_peer.py
View file @
db615fab
...
...
@@ -7,7 +7,6 @@ from collections import namedtuple
import
copy
import
json
from
openassessment.assessment
import
peer_api
from
submissions
import
api
as
submission_api
from
.base
import
XBlockHandlerTestCase
,
scenario
...
...
@@ -97,7 +96,7 @@ class TestPeerAssessment(XBlockHandlerTestCase):
self
.
assertTrue
(
resp
[
'success'
])
# Retrieve the assessment and check that it matches what we sent
actual
=
peer_api
.
get_assessments
(
submission
[
'uuid'
])
actual
=
peer_api
.
get_assessments
(
submission
[
'uuid'
]
,
scored_only
=
False
)
self
.
assertEqual
(
len
(
actual
),
1
)
self
.
assertEqual
(
actual
[
0
][
'submission_uuid'
],
assessment
[
'submission_uuid'
])
self
.
assertEqual
(
actual
[
0
][
'points_earned'
],
5
)
...
...
apps/openassessment/xblock/test/test_self.py
View file @
db615fab
...
...
@@ -35,7 +35,7 @@ class TestSelfAssessment(XBlockHandlerTestCase):
self
.
assertTrue
(
resp
[
'success'
])
# Expect that a self-assessment was created
_
,
assessment
=
self_api
.
get_submission_and
_assessment
(
submission
[
"uuid"
])
assessment
=
self_api
.
get
_assessment
(
submission
[
"uuid"
])
self
.
assertEqual
(
assessment
[
'submission_uuid'
],
submission
[
'uuid'
])
self
.
assertEqual
(
assessment
[
'points_earned'
],
5
)
self
.
assertEqual
(
assessment
[
'points_possible'
],
6
)
...
...
@@ -117,7 +117,7 @@ class TestSelfAssessment(XBlockHandlerTestCase):
# Simulate an error and expect a failure response
with
mock
.
patch
(
'openassessment.xblock.self_assessment_mixin.self_api'
)
as
mock_api
:
mock_api
.
SelfAssessmentRequestError
=
self_api
.
SelfAssessmentRequestError
mock_api
.
get_
submission_and_
assessment
.
side_effect
=
self_api
.
SelfAssessmentRequestError
mock_api
.
get_assessment
.
side_effect
=
self_api
.
SelfAssessmentRequestError
resp
=
self
.
request
(
xblock
,
'render_self_assessment'
,
json
.
dumps
(
dict
()))
self
.
assertIn
(
"error"
,
resp
.
lower
())
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment