Repository: inducer/relate

Issue:
Feature request: see unstarted user in single opportunity grading view
Along with functionality of sorting those users in datatables.


Discussion:
Good idea.

Resolving patch:
diff --git a/course/grades.py b/course/grades.py
--- a/course/grades.py
+++ b/course/grades.py
@@ -345,12 +345,32 @@ def export_gradebook_csv(pctx):
 # {{{ grades by grading opportunity
 
 class OpportunitySessionGradeInfo(object):
-    def __init__(self, grade_state_machine, flow_session, grades=None):
-        # type: (GradeStateMachine, Optional[FlowSession], Optional[Any]) ->  None
-
+    def __init__(self,
+                 grade_state_machine,  # Optional[GradeStateMachine]
+                 flow_session,  # Optional[FlowSession]
+                 flow_id=None,  # Optional[Text]
+                 grades=None,  # Optional[Any]
+                 has_finished_session=False,  # bool
+                 ):
+        # type: (...) ->  None
+        """
+        :param grade_state_machine: a :class:`GradeStateMachine:` or None.
+        :param flow_session: a :class:`FlowSession:` or None.
+        :param flow_id: optional, a :class:`str:` or None. This is used to determine
+        whether the instance is created by a flow-session-related opportunity.
+        :param grades: optional, a :class:`list:` of float or None, representing the
+        percentage grades of each page in the flow session.
+        :param has_finished_session:  a :class:`bool:`, respresent whether
+        the related participation has finished a flow-session, if the opportunity
+        is a flow-session related one. This is used to correctly order flow state,
+        if the participation has at least one finished flow session, the in-progress
+        flow session won't be ordered to top of the session state column.
+        """
         self.grade_state_machine = grade_state_machine
         self.flow_session = flow_session
+        self.flow_id = flow_id
         self.grades = grades
+        self.has_finished_session = has_finished_session
 
 
 class ModifySessionsForm(StyledForm):
@@ -577,6 +597,7 @@ def view_grades_by_opportunity(pctx, opp_id):
                 fsess_idx += 1
 
             my_flow_sessions = []
+
             while (
                     fsess_idx < len(flow_sessions) and
                     flow_sessions[fsess_idx].participation is not None and
@@ -584,11 +605,19 @@ def view_grades_by_opportunity(pctx, opp_id):
                 my_flow_sessions.append(flow_sessions[fsess_idx])
                 fsess_idx += 1
 
+            # When view_page_grades, participations with no started flow sessions
+            # should not be included
+            if not my_flow_sessions and not view_page_grades:
+                grade_table.append(
+                        (participation, OpportunitySessionGradeInfo(
+                            grade_state_machine=None,
+                            flow_id=opportunity.flow_id,
+                            flow_session=None)))
+
             for fsession in my_flow_sessions:
                 total_sessions += 1
 
-                if fsession is None:
-                    continue
+                assert fsession is not None
 
                 if not fsession.in_progress:
                     finished_sessions += 1
@@ -596,14 +625,17 @@ def view_grades_by_opportunity(pctx, opp_id):
                 grade_table.append(
                         (participation, OpportunitySessionGradeInfo(
                             grade_state_machine=state_machine,
-                            flow_session=fsession)))
+                            flow_id=opportunity.flow_id,
+                            flow_session=fsession,
+                            has_finished_session=bool(finished_sessions))))
 
-    if view_page_grades and len(grade_table) > 0 and all(
-            info.flow_session is not None for _dummy1, info in grade_table):
+    if view_page_grades and len(grade_table) > 0 and opportunity.flow_id is not None:
         # Query grades for flow pages
         all_flow_sessions = [
                 cast(FlowSession, info.flow_session)
                 for _dummy1, info in grade_table]
+
+        assert all(all_flow_sessions)
         max_page_count = max(fsess.page_count for fsess in all_flow_sessions)
         page_numbers = list(range(1, 1 + max_page_count))
 


Test patch:
diff --git a/tests/test_grades/test_grades.py b/tests/test_grades/test_grades.py
--- a/tests/test_grades/test_grades.py
+++ b/tests/test_grades/test_grades.py
@@ -869,9 +869,52 @@ def test_post_form_invalid(self):
         self.assertEqual(
             self.mock_recalculate_ended_sessions.call_count, 0)
 
-    def test_get(self):
+    def test_get_flow_status(self):
+        factories.FlowSessionFactory(participation=self.student_participation,
+                                     in_progress=True, page_count=13)
+
+        # There're 3 participations, student has 2 finished session,
+        # 1 in-progress session
+
+        not_started = '<span class="label label-danger">not started</span>'
+        finished = '<span class="label label-success">finished</span>'
+        unfinished = '<span class="label label-warning">unfinished</span>'
+
         resp = self.get_gradebook_by_opp_view(self.gopp_id)
         self.assertEqual(resp.status_code, 200)
+        # The instructor and ta didn't start the session
+        self.assertContains(resp, not_started, count=2, html=True)
+
+        self.assertContains(resp, finished, count=2, html=True)
+        self.assertContains(resp, unfinished, count=1, html=True)
+
+        resp = self.get_gradebook_by_opp_view(self.gopp_id, view_page_grades=True)
+        self.assertEqual(resp.status_code, 200)
+
+        # The student_participation has 2 session in setUp
+        self.assertContains(resp, finished, count=2, html=True)
+        self.assertContains(resp, unfinished, count=1, html=True)
+
+        # no "not started" when view_page_grades
+        self.assertContains(resp, not_started, count=0, html=True)
+
+        # remove all flow sessions
+        for fs in models.FlowSession.objects.all():
+            fs.delete()
+
+        resp = self.get_gradebook_by_opp_view(self.gopp_id)
+        self.assertEqual(resp.status_code, 200)
+        self.assertContains(resp, not_started, count=3, html=True)
+        self.assertContains(resp, finished, count=0, html=True)
+        self.assertContains(resp, unfinished, count=0, html=True)
+
+        resp = self.get_gradebook_by_opp_view(self.gopp_id, view_page_grades=True)
+        self.assertEqual(resp.status_code, 200)
+
+        # no "not started" when view_page_grades
+        self.assertContains(resp, not_started, count=0, html=True)
+        self.assertContains(resp, finished, count=0, html=True)
+        self.assertContains(resp, unfinished, count=0, html=True)
 
     def test_get_with_multiple_flow_sessions(self):
         factories.FlowSessionFactory(