From 9e58ef692dc2a169bf55df458da0e5bf0a5554e1 Mon Sep 17 00:00:00 2001 From: Carson Gee <x@carsongee.com> Date: Fri, 14 Feb 2014 21:51:45 +0000 Subject: [PATCH] fix raw grade dump in standard instructor dashboard Added test and cleaned up code formatting Review based changes Converting class method and attributes to private --- .../tests/test_legacy_raw_download_csv.py | 58 +++++++++ lms/djangoapps/instructor/views/legacy.py | 113 +++++++++++++++--- 2 files changed, 153 insertions(+), 18 deletions(-) create mode 100644 lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py diff --git a/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py b/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py new file mode 100644 index 00000000000..cb7aa803d0e --- /dev/null +++ b/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Create course and answer a problem to test raw grade CSV +""" + +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse + +from courseware.tests.test_submitting_problems import TestSubmittingProblems +from student.roles import CourseStaffRole + + +class TestRawGradeCSV(TestSubmittingProblems): + """ + Tests around the instructor dashboard raw grade CSV + """ + + def setUp(self): + """ + Set up a simple course for testing basic grading functionality. + """ + super(TestRawGradeCSV, self).setUp() + + self.instructor = 'view2@test.com' + self.create_account('u2', self.instructor, self.password) + self.activate_user(self.instructor) + CourseStaffRole(self.course.location).add_users(User.objects.get(email=self.instructor)) + self.logout() + self.login(self.instructor, self.password) + self.enroll(self.course) + + # set up a simple course with four problems + self.homework = self.add_graded_section_to_course('homework', late=False, reset=False, showanswer=False) + self.add_dropdown_to_section(self.homework.location, 'p1', 1) + self.add_dropdown_to_section(self.homework.location, 'p2', 1) + self.add_dropdown_to_section(self.homework.location, 'p3', 1) + self.refresh_course() + + def test_download_raw_grades_dump(self): + """ + Grab raw grade report and make sure all grades are reported. + """ + # Answer second problem correctly with 2nd user to expose bug + self.login(self.instructor, self.password) + resp = self.submit_question_answer('p2', {'2_1': 'Correct'}) + self.assertEqual(resp.status_code, 200) + + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + msg = "url = {0}\n".format(url) + response = self.client.post(url, {'action': 'Download CSV of all RAW grades'}) + msg += "instructor dashboard download raw csv grades: response = '{0}'\n".format(response) + body = response.content.replace('\r', '') + msg += "body = '{0}'\n".format(body) + expected_csv = '''"ID","Username","Full Name","edX email","External email","p3","p2","p1" +"1","u1","username","view@test.com","","None","None","None" +"2","u2","username","view2@test.com","","0.0","1.0","0.0" +''' + self.assertEqual(body, expected_csv, msg) diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 659b3ee25e9..454104a3449 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -1,6 +1,7 @@ """ Instructor Views """ +from contextlib import contextmanager import csv import json import logging @@ -1154,6 +1155,74 @@ def remove_user_from_role(request, username_or_email, role, group_title, event_n return '<font color="green">Removed {0} from {1}</font>'.format(user, group_title) +class GradeTable(object): + """ + Keep track of grades, by student, for all graded assignment + components. Each student's grades are stored in a list. The + index of this list specifies the assignment component. Not + all lists have the same length, because at the start of going + through the set of grades, it is unknown what assignment + compoments exist. This is because some students may not do + all the assignment components. + + The student grades are then stored in a dict, with the student + id as the key. + """ + def __init__(self): + self.components = OrderedDict() + self.grades = {} + self._current_row = {} + + def _add_grade_to_row(self, component, score): + """Creates component if needed, and assigns score + + Args: + component (str): Course component being graded + score (float): Score of student on component + + Returns: + None + """ + component_index = self.components.setdefault(component, len(self.components)) + self._current_row[component_index] = score + + @contextmanager + def add_row(self, student_id): + """Context management for a row of grades + + Uses a new dictionary to get all grades of a specified student + and closes by adding that dict to the internal table. + + Args: + student_id (str): Student id that is having grades set + + """ + self._current_row = {} + yield self._add_grade_to_row + self.grades[student_id] = self._current_row + + def get_grade(self, student_id): + """Retrieves padded list of grades for specified student + + Args: + student_id (str): Student ID for desired grades + + Returns: + list: Ordered list of grades for student + + """ + row = self.grades.get(student_id, []) + ncomp = len(self.components) + return [row.get(comp, None) for comp in range(ncomp)] + + def get_graded_components(self): + """ + Return a list of components that have been + discovered so far. + """ + return self.components.keys() + + def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False, use_offline=False): ''' Return data arrays with student identity and grades for specified course. @@ -1178,20 +1247,12 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, ).prefetch_related("groups").order_by('username') header = [_('ID'), _('Username'), _('Full Name'), _('edX email'), _('External email')] - assignments = [] - if get_grades and enrolled_students.count() > 0: - # just to construct the header - gradeset = student_grades(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline) - # log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset)) - if get_raw_scores: - assignments += [score.section for score in gradeset['raw_scores']] - else: - assignments += [x['label'] for x in gradeset['section_breakdown']] - header += assignments - datatable = {'header': header, 'assignments': assignments, 'students': enrolled_students} + datatable = {'header': header, 'students': enrolled_students} data = [] + gtab = GradeTable() + for student in enrolled_students: datarow = [student.id, student.username, student.profile.name, student.email] try: @@ -1202,15 +1263,31 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, if get_grades: gradeset = student_grades(student, request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline) log.debug('student={0}, gradeset={1}'.format(student, gradeset)) - if get_raw_scores: - # TODO (ichuang) encode Score as dict instead of as list, so score[0] -> score['earned'] - sgrades = [(getattr(score, 'earned', '') or score[0]) for score in gradeset['raw_scores']] - else: - sgrades = [x['percent'] for x in gradeset['section_breakdown']] - datarow += sgrades - student.grades = sgrades # store in student object + with gtab.add_row(student.id) as add_grade: + if get_raw_scores: + # TODO (ichuang) encode Score as dict instead of as list, so score[0] -> score['earned'] + for score in gradeset['raw_scores']: + add_grade(score.section, getattr(score, 'earned', score[0])) + else: + for grade_item in gradeset['section_breakdown']: + add_grade(grade_item['label'], grade_item['percent']) + student.grades = gtab.get_grade(student.id) data.append(datarow) + + # if getting grades, need to do a second pass, and add grades to each datarow; + # on the first pass we don't know all the graded components + if get_grades: + for datarow in data: + # get grades for student + sgrades = gtab.get_grade(datarow[0]) + datarow += sgrades + + # get graded components and add to table header + assignments = gtab.get_graded_components() + header += assignments + datatable['assignments'] = assignments + datatable['data'] = data return datatable -- GitLab