diff --git a/course/grades.py b/course/grades.py index 8006464082cbaf68a91922e650528af4c4274da3..ccfb1aa0ff9332ed1c6dae3717580f670dd5f00d 100644 --- a/course/grades.py +++ b/course/grades.py @@ -34,6 +34,7 @@ from django.db import connection from django import forms from django.db import transaction from django.utils.timezone import now +from django import http from courseflow.utils import StyledForm from crispy_forms.layout import Submit @@ -189,32 +190,26 @@ class GradeInfo: self.grade_state_machine = grade_state_machine -@course_view -def view_gradebook(pctx): - if pctx.role not in [ - participation_role.instructor, - participation_role.teaching_assistant]: - raise PermissionDenied("must be instructor or TA to view grades") - +def get_grade_table(course): # NOTE: It's important that these queries are sorted consistently, # also consistently with the code below. grading_opps = list((GradingOpportunity.objects .filter( - course=pctx.course, + course=course, shown_in_grade_book=True, ) .order_by("identifier"))) participations = list(Participation.objects .filter( - course=pctx.course, + course=course, status=participation_status.active) .order_by("id") .select_related("user")) grade_changes = list(GradeChange.objects .filter( - opportunity__course=pctx.course, + opportunity__course=course, opportunity__shown_in_grade_book=True) .order_by( "participation__id", @@ -260,6 +255,18 @@ def view_gradebook(pctx): grade_table.append(grade_row) + return participations, grading_opps, grade_table + + +@course_view +def view_gradebook(pctx): + if pctx.role not in [ + participation_role.instructor, + participation_role.teaching_assistant]: + raise PermissionDenied("must be instructor or TA to view grades") + + participations, grading_opps, grade_table = get_grade_table(pctx.course) + grade_table = sorted(zip(participations, grade_table), key=lambda (participation, grades): (participation.user.last_name.lower(), @@ -272,6 +279,38 @@ def view_gradebook(pctx): "grade_state_change_types": grade_state_change_types, }) + +@course_view +def export_gradebook_csv(pctx): + if pctx.role not in [ + participation_role.instructor, + participation_role.teaching_assistant]: + raise PermissionDenied("must be instructor or TA to export grades") + + participations, grading_opps, grade_table = get_grade_table(pctx.course) + + from six import BytesIO + csvfile = BytesIO() + + import unicodecsv + fieldnames = ['user_name', 'last_name', 'first_name'] + [ + gopp.identifier for gopp in grading_opps] + + writer = unicodecsv.writer(csvfile, encoding="utf-8") + writer.writerow(fieldnames) + + for participation, grades in zip(participations, grade_table): + writer.writerow([ + participation.user.username, + participation.user.last_name, + participation.user.first_name, + ] + [grade_info.grade_state_machine.stringify_machine_readable_state() + for grade_info in grades]) + + return http.HttpResponse( + csvfile.getvalue(), + content_type="text/plain; charset=utf-8") + # }}} diff --git a/course/models.py b/course/models.py index c77d8f115850f04c56163052c333d3deaa5fc1b9..1061d73ff4bf6383f7ce1cc636090a9208a9f3ca 100644 --- a/course/models.py +++ b/course/models.py @@ -750,6 +750,19 @@ class GradeStateMachine(object): else: return "(other state)" + def stringify_machine_readable_state(self): + if self.state is None: + return u"NONE" + elif self.state == grade_state_change_types.exempt: + return "EXEMPT" + elif self.state == grade_state_change_types.graded: + if self.valid_percentages: + return "%.3f" % self.percentage() + else: + return u"NONE" + else: + return u"OTHER_STATE" + def stringify_percentage(self): if self.state == grade_state_change_types.graded: if self.valid_percentages: diff --git a/course/templates/course/course-base.html b/course/templates/course/course-base.html index ec9d0bc5eb8eb05377341da8cd06c95c4160398a..53190fa8ad06c435fdb60abd20fb4214d3cccff5 100644 --- a/course/templates/course/course-base.html +++ b/course/templates/course/course-base.html @@ -43,6 +43,7 @@