docs: def _spreadsheet2gradebook_multi(
              self,
              csv_reader,
              email_field,
              non_assignment_fields,
              approve_grades=False,
              use_max_points_column=False,
              max_points_column=None,
              normalize_column=None
      ):
          """Transfer grades from spreadsheet to array.
  
          Helper function that transfer grades from spreadsheet using
          ``multi_grade()`` (multiple students at a time). We do this by
          creating a large array containing all grades to transfer, then
          make one call to the Gradebook API.
  
          Args:
              csv_reader (csv.DictReader): list of rows in CSV file
              email_field (str): The name of the email field
              non_assignment_fields (list): list of column names in CSV file
                  which should not be treated as assignment names
              approve_grades (bool): Should grades be auto approved?
              use_max_points_column (bool): If true, read the max points
                  and normalize values from the CSV and use the max points value
                  in place of the default if normalized is False.
              max_points_column (str): The name of the max_pts column. All
                  rows contain the same number, the max points for
                  the assignment.
              normalize_column (str): The name of the normalize column which
                  indicates whether to use the max points value.
  
          Raises:
              PyLmodFailedAssignmentCreation: Failed to create assignment
              requests.RequestException: Exception connection error
              ValueError: Unable to decode response content
  
          Returns:
              tuple: tuple of dictionary containing response ``status``
              and ``message``, and duration of operation
  
          """
          # pylint: disable=too-many-locals
          if use_max_points_column:
              if max_points_column is None:
                  raise ValueError(
                      "max_points_column must be set "
                      "if use_max_points_column is set"
                  )
              if normalize_column is None:
                  raise ValueError(
                      "normalize_column must be set "
                      "if use_max_points_column is set"
                  )
  
          assignments = self.get_assignments()
          students = self.get_students()
          assignment2id = {}
          grade_array = []
          for row in csv_reader:
              email = row[email_field]
              sid, _ = self.get_student_by_email(email, students)
              if sid is None:
                  log.warning(
                      'Error in spreadsheet2gradebook: cannot find '
                      'student id for email="%s"\n', email
                  )
                  continue
              for field in row.keys():
                  if field in non_assignment_fields:
                      continue
                  if field not in assignment2id:
                      assignment_id, _ = self.get_assignment_by_name(
                          field, assignments=assignments
                      )
                      # If no assignment found, try creating it.
                      if assignment_id is None:
                          name = field
                          shortname = field[0:3] + field[-2:]
                          log.info('calling create_assignment from multi')
  
                          # If the max_pts and normalize columns are present,
                          # and use_max_points_column is True,
                          # replace the default value for max points.
                          max_points = DEFAULT_MAX_POINTS
                          if use_max_points_column:
                              # This value means it was already normalized, and
                              # we should use the default max points
                              # instead of the one in the CSV.
                              normalize_value = True
                              normalize_value_str = row.get(normalize_column)
                              if normalize_value_str is not None:
                                  try:
                                      normalize_value = bool(
                                          int(normalize_value_str)
                                      )
                                  except ValueError as ex:
                                      # Value is already normalized
                                      log.warning(
                                          'Bool conversion error '
                                          ' in normalize column for '
                                          'value: %s, exception: %s',
                                          normalize_value_str,
                                          ex
                                      )
  
                              if not normalize_value:
                                  max_points_str = row.get(max_points_column)
                                  if max_points_str is not None:
                                      try:
                                          max_points = float(max_points_str)
                                      except ValueError as ex:
                                          log.warning(
                                              'Floating point conversion error '
                                              'in max points column for '
                                              'value: %s, exception: %s',
                                              max_points_str,
                                              ex
                                          )
  
                          response = self.create_assignment(
                              name, shortname, 1.0, max_points, '12-15-2013'
                          )
                          if (
                                  not response.get('data', '') or
                                  'assignmentId' not in response.get('data')
                          ):
                              failure_message = (
                                  "Error! Failed to create assignment {0}"
                                  ", got {1}".format(
                                      name, response
                                  )
                              )
                              log.critical(failure_message)
                              raise PyLmodFailedAssignmentCreation(
                                  failure_message
                              )
                          assignment_id = response['data']['assignmentId']
                      log.info("Assignment %s has Id=%s", field, assignment_id)
                      assignment2id[field] = assignment_id
  
                  assignment_id = assignment2id[field]
                  successful = True
                  try:
                      # Try to convert to numeric, but grade the
                      # rest anyway if any particular grade isn't a
                      # number
                      gradeval = float(row[field]) * 1.0
                      log.debug(
                          'Received grade value %s(converted to %s) for '
                          'student %s on assignment %s',
                          row[field],
                          gradeval,
                          sid,
                          assignment_id,
                      )
                  except ValueError as err:
                      log.exception(
                          "Failed in converting grade for student %s"
                          ", row=%s, err=%s", sid, row, err
                      )
                      successful = False
                  if successful:
                      grade_array.append({
                          "studentId": sid,
                          "assignmentId": assignment_id,
                          "numericGradeValue": gradeval,
                          "mode": 2,
                          "isGradeApproved": approve_grades
                      })
          # Everything is setup to post, do the post and track the time
          # it takes.
          log.info(
              'Data read from file, doing multiGrades API '
              'call (%d grades)', len(grade_array)
          )
          tstart = time.time()
          response = self.multi_grade(grade_array)
          duration = time.time() - tstart
          log.info(
              'multiGrades API call done (%d bytes returned) '
              'dt=%6.2f seconds.', len(json.dumps(response)), duration
          )
          return response, duration