Commit 1747a2ef by David Ormsbee

Merge pull request #261 from edx/ormsbee/admin_screens

A more useful admin interface
parents 7f1bbf0b 37bbf221
import json
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils import html
from openassessment.assessment.models import (
Assessment, AssessmentPart, Rubric,
AssessmentFeedback, AssessmentFeedbackOption,
Criterion, CriterionOption,
PeerWorkflow, PeerWorkflowItem,
Assessment, AssessmentFeedback, PeerWorkflow, PeerWorkflowItem, Rubric
)
from openassessment.assessment.serializers import RubricSerializer
class RubricAdmin(admin.ModelAdmin):
list_per_page = 20 # Loads of criteria summary are moderately expensive
list_display = ('id', 'content_hash', 'criteria_summary')
list_display_links = ('id', 'content_hash')
search_fields = ('id', 'content_hash')
readonly_fields = (
'id', 'content_hash', 'points_possible', 'criteria_summary', 'data'
)
def criteria_summary(self, rubric_obj):
"""Short description of criteria for presenting in a list."""
rubric_data = RubricSerializer.serialized_from_cache(rubric_obj)
return u", ".join(
u"{}: {}".format(criterion["name"], criterion["points_possible"])
for criterion in rubric_data["criteria"]
)
def data(self, rubric_obj):
"""Full JSON string of rubric, indented and HTML formatted."""
rubric_data = RubricSerializer.serialized_from_cache(rubric_obj)
return u"<pre>\n{}\n</pre>".format(
html.escape(json.dumps(rubric_data, sort_keys=True, indent=4))
)
data.allow_tags = True
class PeerWorkflowItemInline(admin.StackedInline):
model = PeerWorkflowItem
fk_name = 'author'
raw_id_fields = ('author', 'scorer', 'assessment')
extra = 0
class PeerWorkflowAdmin(admin.ModelAdmin):
list_display = (
'id', 'student_id', 'item_id', 'course_id', 'submission_uuid',
'created_at', 'completed_at', 'grading_completed_at',
)
search_fields = (
'id', 'student_id', 'item_id', 'course_id', 'submission_uuid',
)
inlines = (PeerWorkflowItemInline,)
class AssessmentAdmin(admin.ModelAdmin):
list_display = (
'id', 'submission_uuid', 'score_type', 'scorer_id', 'scored_at',
'rubric_link',
)
search_fields = (
'id', 'submission_uuid', 'score_type', 'scorer_id', 'scored_at',
'rubric__content_hash',
)
readonly_fields = (
'submission_uuid', 'rubric_link', 'scored_at', 'scorer_id',
'score_type', 'points_earned', 'points_possible', 'feedback',
'parts_summary',
)
exclude = ('rubric', 'submission_uuid')
def rubric_link(self, assessment_obj):
url = reverse(
'admin:assessment_rubric_change',
args=[assessment_obj.rubric.id]
)
return u'<a href="{}">{}</a>'.format(
url, assessment_obj.rubric.content_hash
)
rubric_link.allow_tags = True
rubric_link.admin_order_field = 'rubric__content_hash'
rubric_link.short_description = 'Rubric'
def parts_summary(self, assessment_obj):
return "<br/>".join(
html.escape(
u"{}/{} - {}: {}".format(
part.points_earned,
part.points_possible,
part.option.criterion.name,
part.option.name,
)
)
for part in assessment_obj.parts.all()
)
parts_summary.allow_tags = True
class AssessmentFeedbackAdmin(admin.ModelAdmin):
list_display = ('id', 'submission_uuid',)
search_fields = ('id', 'submission_uuid',)
readonly_fields = (
'submission_uuid', 'assessments_by', 'options', 'feedback_text'
)
exclude = ('assessments',)
def assessments_by(self, assessment_feedback):
links = [
u'<a href="{}">{}</a>'.format(
reverse('admin:assessment_assessment_change', args=[asmt.id]),
html.escape(asmt.scorer_id)
)
for asmt in assessment_feedback.assessments.all()
]
return ", ".join(links)
assessments_by.allow_tags = True
admin.site.register(Assessment)
admin.site.register(AssessmentPart)
admin.site.register(AssessmentFeedback)
admin.site.register(AssessmentFeedbackOption)
admin.site.register(Rubric)
admin.site.register(Criterion)
admin.site.register(CriterionOption)
admin.site.register(PeerWorkflow)
admin.site.register(PeerWorkflowItem)
admin.site.register(Rubric, RubricAdmin)
admin.site.register(PeerWorkflow, PeerWorkflowAdmin)
admin.site.register(Assessment, AssessmentAdmin)
admin.site.register(AssessmentFeedback, AssessmentFeedbackAdmin)
......@@ -237,7 +237,7 @@ class Assessment(models.Model):
objects that map to each :class:`Criterion` in the :class:`Rubric` we're
assessing against.
"""
MAXSIZE = 1024 * 100 # 100KB
MAXSIZE = 1024 * 100 # 100KB
submission_uuid = models.CharField(max_length=128, db_index=True)
rubric = models.ForeignKey(Rubric)
......@@ -433,6 +433,9 @@ class AssessmentFeedbackOption(models.Model):
"""
text = models.CharField(max_length=255, unique=True)
def __unicode__(self):
return u'"{}"'.format(self.text)
class AssessmentFeedback(models.Model):
"""
......
......@@ -3,8 +3,17 @@ from django.contrib import admin
from .models import AssessmentWorkflow
class AssessmentWorkflowAdmin(admin.ModelAdmin):
"""Admin for the user's overall workflow through open assessment.
Unlike many of the other models, we allow editing here. This is so that we
can manually move a user's entry to "done" and give them a separate score
in the submissions app if that's required. Unlike rubrics and assessments,
there is no expectation of immutability for `AssessmentWorkflow`.
"""
list_display = (
'uuid', 'status', 'status_changed', 'submission_uuid', 'score'
'uuid', 'status', 'submission_uuid', 'course_id', 'item_id', 'status_changed'
)
list_filter = ('status',)
search_fields = ('uuid', 'submission_uuid', 'course_id', 'item_id')
admin.site.register(AssessmentWorkflow, AssessmentWorkflowAdmin)
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils import html
from submissions.models import Score, StudentItem, Submission
class SubmissionAdmin(admin.ModelAdmin):
class StudentItemAdminMixin(object):
"""Mix this class into anything that has a student_item fkey."""
search_fields = (
'student_item__course_id',
'student_item__student_id',
'student_item__item_id',
'student_item__id'
)
def course_id(self, obj):
return obj.student_item.course_id
course_id.admin_order_field = 'student_item__course_id'
def item_id(self, obj):
return obj.student_item.item_id
item_id.admin_order_field = 'student_item__item_id'
def student_id(self, obj):
return obj.student_item.student_id
student_id.admin_order_field = 'student_item__student_id'
def student_item_id(self, obj):
url = reverse(
'admin:submissions_studentitem_change',
args=[obj.student_item.id]
)
return u'<a href="{}">{}</a>'.format(url, obj.student_item.id)
student_item_id.allow_tags = True
student_item_id.admin_order_field = 'student_item__id'
student_item_id.short_description = 'S.I. ID'
class StudentItemAdmin(admin.ModelAdmin):
list_display = ('id', 'course_id', 'item_type', 'item_id', 'student_id')
list_filter = ('item_type',)
search_fields = ('id', 'course_id', 'item_type', 'item_id', 'student_id')
readonly_fields = ('course_id', 'item_type', 'item_id', 'student_id')
class SubmissionAdmin(admin.ModelAdmin, StudentItemAdminMixin):
list_display = (
'student_item', 'uuid', 'attempt_number', 'submitted_at', 'created_at',
'raw_answer', 'scores'
'id', 'uuid',
'course_id', 'item_id', 'student_id', 'student_item_id',
'attempt_number', 'submitted_at',
)
list_display_links = ('id', 'uuid')
list_filter = ('student_item__item_type',)
readonly_fields = (
'student_item_id',
'course_id', 'item_id', 'student_id',
'attempt_number', 'submitted_at', 'created_at',
'raw_answer', 'all_scores',
)
search_fields = ('id', 'uuid') + StudentItemAdminMixin.search_fields
# We're creating our own explicit link and displaying parts of the
# student_item in separate fields -- no need to display this as well.
exclude = ('student_item',)
def scores(self, obj):
return ", ".join(
"{}/{}".format(score.points_earned, score.points_possible)
for score in Score.objects.filter(submission=obj.id)
def all_scores(self, submission):
return "\n".join(
"{}/{} - {}".format(
score.points_earned, score.points_possible, score.created_at
)
for score in Score.objects.filter(submission=submission)
)
admin.site.register(Score)
admin.site.register(StudentItem)
class ScoreAdmin(admin.ModelAdmin, StudentItemAdminMixin):
list_display = (
'id',
'course_id', 'item_id', 'student_id', 'student_item_id',
'points', 'created_at'
)
list_filter = ('student_item__item_type',)
readonly_fields = (
'student_item_id',
'student_item',
'submission',
'points_earned',
'points_possible',
'reset',
)
search_fields = ('id', ) + StudentItemAdminMixin.search_fields
def points(self, score):
return u"{}/{}".format(score.points_earned, score.points_possible)
admin.site.register(Score, ScoreAdmin)
admin.site.register(StudentItem, StudentItemAdmin)
admin.site.register(Submission, SubmissionAdmin)
......@@ -100,12 +100,21 @@ class Submission(models.Model):
raw_answer=self.raw_answer,
))
def __unicode__(self):
return u"Submission {}".format(self.uuid)
class Meta:
ordering = ["-submitted_at", "-id"]
class Score(models.Model):
"""What the user scored for a given StudentItem at a given time."""
"""What the user scored for a given StudentItem at a given time.
Note that while a Score *can* be tied to a Submission, it doesn't *have* to.
Specifically, if we want to have scores for things that are not a part of
the courseware (like "class participation"), there would be no corresponding
Submission.
"""
student_item = models.ForeignKey(StudentItem)
submission = models.ForeignKey(Submission, null=True)
points_earned = models.PositiveIntegerField(default=0)
......
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