Repository: BraeWebb/allocate

Issue:
Stub Tutors and Sessions
Ability to use an availability file to create empty files with the list of sessions and tutors filled out.

```bash
allocate --stub tutors.csv sessions.csv availability.csv
```

Comments:

Patch:
diff --git a/README.md b/README.md
index 5f97814..7d8a697 100644
--- a/README.md
+++ b/README.md
@@ -92,8 +92,8 @@ allocate tutors.csv sessions.csv availability.csv
 ```
 ```
 Henry,T01,P01,P02,P03,P05
-Brae,T04,P01,P03,P04,P05
-Emily,T02,T03,P02,P04
+Brae,T02,T04,P01,P03,P04,P05
+Emily,T03,P02,P04
 ```
 
 ## Support
diff --git a/allocate/allocation.py b/allocate/allocation.py
index 4559ede..cf9989f 100644
--- a/allocate/allocation.py
+++ b/allocate/allocation.py
@@ -7,7 +7,7 @@ from typing import Dict, List
 from allocate.solver import validate_availability, Engine
 from allocate.model import Tutor, Session
 from allocate.csvalidator import CSVModel
-from allocate.doodle import parse_doodle_hack
+from allocate.doodle import parse_doodle_to_stub, parse_doodle_hack
 
 
 def solution_to_csv(solution: Dict[str, List[str]], output):
@@ -20,6 +20,31 @@ def solution_to_csv(solution: Dict[str, List[str]], output):
         writer.writerow([name, *sessions])
 
 
+def stub_files(tutors: str, sessions: str, availability: str):
+    """Write out stub files for tutors and sessions based on the
+    given availability file."""
+    tutors_names, sessions_details = parse_doodle_to_stub(availability)
+
+    with open(tutors, 'x') as file:
+        writer = csv.writer(file)
+
+        # write header
+        writer.writerow(Tutor.__annotations__.keys())
+        columns = len(Tutor.__annotations__)
+        # write tutor names
+        for tutor in sorted(tutors_names):
+            writer.writerow([tutor] + ["" for _ in range(columns - 1)])
+
+    with open(sessions, 'x') as file:
+        writer = csv.writer(file)
+
+        # write header
+        writer.writerow(Session.__annotations__.keys())
+        # write session names
+        for session in sessions_details:
+            writer.writerow(["", session.day, session.start, session.duration, "", ""])
+
+
 def run_allocation(tutors: str, sessions: str, availability: str,
                    json: bool = False):
     tutor_model = CSVModel(Tutor)
@@ -58,12 +83,17 @@ def main():
     parser.add_argument('availability', type=str,
                         help='CSV file of tutors availabilities to sessions')
 
+    parser.add_argument('--stub', action='store_true',
+                        help='Write to the tutor and session files a stub table generated by the allocations file')
     parser.add_argument('--json', action="store_true",
                         help='Output solution as a JSON object instead of default')
 
     args = parser.parse_args()
 
-    run_allocation(args.tutors, args.sessions, args.availability, json=args.json)
+    if args.stub:
+        stub_files(args.tutors, args.sessions, args.availability)
+    else:
+        run_allocation(args.tutors, args.sessions, args.availability, json=args.json)
 
 
 if __name__ == '__main__':
diff --git a/allocate/doodle.py b/allocate/doodle.py
index b6914d5..885ffeb 100644
--- a/allocate/doodle.py
+++ b/allocate/doodle.py
@@ -75,6 +75,8 @@ def _assign_columns_timeslots(days: Iterable[str], times: Iterable[str]):
     return columns
 
 
+# TODO: Abstract the 3 functions below
+
 def parse_doodle(filename: str) -> Dict[str, List[TimeSlot]]:
     """Parse a Doodle CSV to create a dictionary that maps a tutor name
     to a list of all their available time slots.
@@ -105,6 +107,33 @@ def parse_doodle(filename: str) -> Dict[str, List[TimeSlot]]:
         return dict(availabilities)
 
 
+def parse_doodle_to_stub(filename: str) -> Tuple[Iterable[str], Iterable[TimeSlot]]:
+    """Parse a Doodle CSV to create the lists of tutors and list of sessions
+    in the availability file.
+    """
+    with open(filename, 'r') as file:
+        reader = iter(csv.reader(file))
+
+        # skip the first 4 rows since it is just doodle garbage
+        reader = islice(reader, 4, None)
+
+        day_row = next(reader)
+        time_row = next(reader)
+        days = _assign_columns_timeslots(day_row, time_row)
+
+        tutors = set()
+        sessions = days.values()
+        for row in reader:
+            name = row[0]
+            # last row is always a count of availabilities for a timeslot
+            if name == "Count":
+                break
+
+            tutors.add(name)
+
+        return tutors, sessions
+
+
 def parse_doodle_hack(filename: str, tutors: Iterable[Tutor],
                       sessions: Iterable[Session]) \
         -> Dict[Tuple[Tutor, Session], bool]:
diff --git a/allocate/model.py b/allocate/model.py
index 8c0b007..ceea073 100644
--- a/allocate/model.py
+++ b/allocate/model.py
@@ -28,6 +28,6 @@ class Session:
     id: str
     day: Day
     start_time: int
+    duration: int = 1
     lower_tutor_count: int = 1
     upper_tutor_count: int = 1
-    duration: int = 1


Test patch:
diff --git a/tests/allocation_test.py b/tests/allocation_test.py
index 56dfbec..ec34031 100644
--- a/tests/allocation_test.py
+++ b/tests/allocation_test.py
@@ -1,16 +1,39 @@
 import io
+import os
 from unittest import TestCase
 from contextlib import redirect_stdout
 
-from allocate.allocation import run_allocation
+from allocate.allocation import run_allocation, stub_files
 
 
 class AllocationTest(TestCase):
+    """Allocation tests works as a quick system sanity test"""
+
     EXPECTED_ALLOCATION = """Henry,T01,P01,P02,P03,P05
 Brae,T04,P01,P03,P04,P05
 Emily,T02,T03,P02,P04"""
 
+    EXPECTED_TUTORS_STUB = """name,is_junior,pref_contig,lower_hr_limit,upper_hr_limit,daily_max,session_preference
+Brae,,,,,,
+Emily,,,,,,
+Henry,,,,,,"""
+
+    EXPECTED_SESSIONS_STUB = """id,day,start_time,duration,lower_tutor_count,upper_tutor_count
+,Tue,11,2,,
+,Tue,13,2,,
+,Tue,15,2,,
+,Wed,8,1,,
+,Wed,9,1,,
+,Wed,12,2,,
+,Thu,9,2,,
+,Fri,11,1,,
+,Fri,12,1,,"""
+
     def test_sample_allocation(self):
+        """Tests to ensure standard allocation works as expected
+
+        allocate sample_tutors.csv sample_sessions.csv sample_doodle.csv
+        """
         output = io.StringIO()
         with redirect_stdout(output):
             run_allocation("sample_tutors.csv", "sample_sessions.csv",
@@ -19,3 +42,26 @@ Emily,T02,T03,P02,P04"""
         self.assertEqual(output.getvalue().split(),
                          AllocationTest.EXPECTED_ALLOCATION.split(),
                          "Allocation output does not match")
+
+    def test_stub_generation(self):
+        """Tests to ensure that stubs are generated from availability
+        appropriately
+
+        allocate --stub stub_tutors.csv stub_sessions.csv sample_doodle.csv
+        """
+        stub_files("stub_tutors.csv", "stub_sessions.csv", "sample_doodle.csv")
+
+        with open("stub_tutors.csv") as file:
+            self.assertEqual([line.strip() for line in file.readlines()],
+                             AllocationTest.EXPECTED_TUTORS_STUB.split(),
+                             "Tutor stub file does not match")
+
+        with open("stub_sessions.csv") as file:
+            self.assertEqual([line.strip() for line in file.readlines()],
+                             AllocationTest.EXPECTED_SESSIONS_STUB.split(),
+                             "Session stub file does not match")
+
+    @classmethod
+    def tearDownClass(cls):
+        os.remove("stub_tutors.csv")
+        os.remove("stub_sessions.csv")