Repository: jupyter/nbgrader

Issue:
Partial credit -- assigning zero marks gives full credit
When returning zero from a cell as partial credit, this awards full marks, due to the use of `> 0.0` rather than `>= 0.0` in the code below:

https://github.com/jupyter/nbgrader/blob/ed98bc9ddffec5900f99e310434d6f3c8cedc9b7/nbgrader/utils.py#L87-L94

e.g. consider the following:
```
score = test_one(...)
score += test_two(...)
score += test_three(...)

score
```

This may output zero if all tests fail. 

Discussion:

Patch:
diff --git a/nbgrader/utils.py b/nbgrader/utils.py
index 37abb0f1..7ac70fc3 100644
--- a/nbgrader/utils.py
+++ b/nbgrader/utils.py
@@ -62,34 +62,53 @@ def is_locked(cell: NotebookNode) -> bool:
         return cell.metadata['nbgrader'].get('locked', False)
 
 def get_partial_grade(output, max_points, log=None):
+    """
+    Calculates partial grade for a cell, based on contents of
+    output["data"]["text/plain"]. Returns a value between 0
+    and max_points. Returns max_points (and a warning) for edge cases.
+    """
     # check that output["data"]["text/plain"] exists
     if not output["data"]["text/plain"]:
         raise KeyError("output ['data']['text/plain'] does not exist")
     grade = output["data"]["text/plain"]
-    warning_msg = """For autograder tests, expecting output to indicate
-    partial credit and be single value between 0.0 and max_points.
-    Currently treating other output as full credit, but future releases
-    may treat as error."""
-    # For partial credit, expecting grade to be a value between 0 and max_points
-    # A valid value for key output["data"]["text/plain"] can be a list or a string
+    # For partial credit, expecting grade to be a value between 0
+    # and max_points
+    # A valid value for key output["data"]["text/plain"] can be a
+    # list or a string, so handle the string case
     if (isinstance(grade,list)):
+        # grade is a list
         if (len(grade)>1):
             if log:
+                warning_msg = """Cell output is {}, which is a list. For autograder tests, expecting output to indicate
+                partial credit and be single value between 0.0 and max_points.
+                Currently treating other output as full credit, but future
+                releases may treat as error.""".format(grade)
                 log.warning(warning_msg)
             return max_points
+        # if a single value in list, set grade to that value
         grade = grade[0]
     try:
+        # now that we have a single values for grade, can we
+        # convert to a float?
         grade = float(grade)
     except ValueError:
         if log:
+            warning_msg = """Cell output is {}, which cannot be converted to
+            a float. For
+            autograder tests, expecting output to indicate
+            partial credit and be single value between 0.0 and max_points.
+            Currently treating other output as full credit, but future releases
+            may treat as error.""".format(grade)
             log.warning(warning_msg)
         return max_points
-    if (grade > 0.0):
+    if (grade >= 0.0):
         if (grade > max_points):
             raise ValueError("partial credit cannot be greater than maximum points for cell")
         return grade
     else:
         if log:
+            warning_msg = """Cell output is {}, which is less than 0.0.
+            This is strange.""".format(grade)
             log.warning(warning_msg)
         return max_points
 


Test patch:
diff --git a/nbgrader/tests/utils/test_utils.py b/nbgrader/tests/utils/test_utils.py
index 4e1de7eb..8f4c2ea0 100644
--- a/nbgrader/tests/utils/test_utils.py
+++ b/nbgrader/tests/utils/test_utils.py
@@ -141,6 +141,10 @@ def test_get_partial_grade():
     test_data = { "data": { "text/plain": "6.0" } }
     assert utils.get_partial_grade(test_data,10.0) == 6.0
 
+    # test zero
+    test_data = { "data": { "text/plain": [ "0.0" ] } }
+    assert utils.get_partial_grade(test_data,1.0) == 0.0
+
     # test string in list, should assume partial credit not intended
     test_data = { "data": { "text/plain": [ "'this is a string'" ] } }
     assert utils.get_partial_grade(test_data,2.0) == 2.0
@@ -157,6 +161,8 @@ def test_get_partial_grade():
 def test_determine_grade_code_partial_credit():
     # create grade cell with max_points == 5
     cell = create_grade_cell('test', "code", "foo", 5)
+
+    # simple case when output between 0 and max_points
     test_data = {
         "text/plain": "3"
         }
@@ -180,6 +186,7 @@ def test_determine_grade_code_partial_credit():
     with pytest.raises(ValueError):
         utils.determine_grade(cell)
 
+    # returns max_points if output is a list
     cell.outputs = []
     test_data = {
         "text/plain": [ "0.5", "abc" ]