From caf96ebe7a118a28ff52f035732a75b624f2698c Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Sun, 21 Dec 2014 16:34:43 -0600 Subject: [PATCH] Implement grade export --- course/grades.py | 59 ++++++++++++++++++++---- course/models.py | 13 ++++++ course/templates/course/course-base.html | 1 + courseflow/urls.py | 4 ++ requirements.txt | 3 ++ 5 files changed, 70 insertions(+), 10 deletions(-) diff --git a/course/grades.py b/course/grades.py index 80064640..ccfb1aa0 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 c77d8f11..1061d73f 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 ec9d0bc5..53190fa8 100644 --- a/course/templates/course/course-base.html +++ b/course/templates/course/course-base.html @@ -43,6 +43,7 @@
  • List of Participants
  • List of Grading Opportunities
  • Grade book
  • +
  • Grade book (CSV export)
  • {% endif %} {% if role == pr.instructor %}
  • Import Grades
  • diff --git a/courseflow/urls.py b/courseflow/urls.py index c689297a..110a0c66 100644 --- a/courseflow/urls.py +++ b/courseflow/urls.py @@ -103,6 +103,10 @@ urlpatterns = patterns('', "/(?P[-a-zA-Z0-9]+)" "/grading/overview/$", "course.grades.view_gradebook",), + url(r"^course" + "/(?P[-a-zA-Z0-9]+)" + "/grading/overview/csv/$", + "course.grades.export_gradebook_csv",), url(r"^course" "/(?P[-a-zA-Z0-9]+)" "/grading/by-opportunity" diff --git a/requirements.txt b/requirements.txt index 7541d5a3..6736268c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,3 +52,6 @@ docker-py # For code highlighting, required via the CodeHilite extension to # Python-Markdown pygments + +# For grade export +unicodecsv -- GitLab