Browse Source

initial commit

Justin Tracey 6 years ago
commit
04b494d31f
1 changed files with 247 additions and 0 deletions
  1. 247 0
      calendar.py

+ 247 - 0
calendar.py

@@ -0,0 +1,247 @@
+#!/usr/bin/env python
+from datetime import date,timedelta
+import sys
+import argparse
+
+def str_to_date(string_date):
+    return date(*[int(s) for s in string_date.split('-')])
+
+class CryspCalendar:
+    """Representation of a CrySP weekly meeting schedule for a term"""
+    def __init__(self, start, end=None, weeks=None, names=[],\
+                 constraints=[], practice_talks=[], half_practices=[], notes=[], no_repeat=False):
+        self.dates = self.meeting_dates(start, end, weeks)
+        self.names = names
+        self.constraints = self.parse_constraints(constraints)
+        self.practice_talks      = self.parse_practice_talks(practice_talks)
+        self.practice_talks.update(self.parse_practice_talks(half_practices, half=True))
+        self.notes      = self.parse_notes(notes)
+        self.notes.update(self.parse_notes(practice_talks))
+        self.notes.update(self.parse_notes(half_practices))
+        self.verify_constraints()
+        self.no_repeat = no_repeat
+
+    def meeting_dates(self, start, end=None, weeks=None):
+        first_meeting = str_to_date(start)
+        last_meeting = None
+        if end:
+            last_meeting = str_to_date(end)
+        if weeks:
+            last_meeting = max(first_meeting + timedelta(weeks=weeks), last_meeting)
+        ret = []
+        next_meeting = first_meeting
+        one_week = timedelta(weeks=1)
+        while next_meeting <= last_meeting:
+            ret.append(next_meeting)
+            next_meeting += one_week
+        return ret
+
+    def parse_constraints(self, constraints):
+        constraint_dict = {}
+        for constraint in constraints:
+            name      = constraint[0]
+            date_strs = constraint[1:]
+            dates     = []
+            for date_str in date_strs:
+                dates.append(str_to_date(date_str))
+                constraint_dict[name] = dates
+        return constraint_dict
+
+    def parse_practice_talks(self, practice, half=False):
+        practice_dict = {}
+        for talk in practice:
+            name = talk[0]
+            day  = str_to_date(talk[1])
+            assert half or day not in practice_dict,\
+                "conflicting talks: %s and %s on %s" %\
+                (practice_dict[day][0], name, day)
+            if half:
+                practice_dict[day] = [name,]
+            else:
+                practice_dict[day] = [name, ""]
+        return practice_dict
+
+    def parse_notes(self, notes):
+        notes_dict = {}
+        for note in notes:
+            day     = str_to_date(note[1])
+            message = note[2]
+            assert day not in notes_dict,\
+                'conflicting notes: "%s" and "%s" on %s' %\
+                (notes_dict[day], message, note[1])
+            notes_dict[day] = message
+        return notes_dict
+
+    def verify_constraints(self):
+        for name in self.constraints:
+            for day in self.constraints[name]:
+                assert day in self.dates,\
+                    "constraint added for %s on %s, which is not a meeting date" %\
+                    (name, day)
+        for day in self.practice_talks:
+            assert day in self.dates,\
+                "talk by %s designated on %s, which is not a meeting date" %\
+                (self.practice_talks[day][0], day)
+        for day in self.notes:
+            assert day in self.dates,\
+                'note "%s" designated on %s, which is not a meeting date' %\
+                (self.notes[day], day)
+
+    def pop_practice_talk(self, day, i):
+        if i < len(self.practice_talks[day]):
+            return self.practice_talks[day][i]
+        return None
+
+    def pop_name(self, day, names, i):
+        name = None
+        if day in self.practice_talks:
+            #name = self.practice_talks[day][i]
+            name = self.pop_practice_talk(day, i)
+        if name is None:
+            constrained_names = (name for name in names \
+                                 if name not in self.constraints \
+                                 or day  not in self.constraints[name])
+            name = next(constrained_names, None)
+        if name and name in names:
+            names.remove(name)
+        return name
+
+    def next_speaker(self, day, names, i):
+        name = self.pop_name(day, names, i)
+        if name or self.no_repeat:
+            return name
+        names.extend(self.names)
+        return self.pop_name(day, names, i)
+
+    def date_to_speakers(self, day, names):
+        speaker1 = self.next_speaker(day, names, 0)
+        speaker2 = self.next_speaker(day, names, 1)
+        note = self.notes.get(day, "")
+        return speaker1, speaker2, note
+
+    # returns a string containing a twiki table
+    def table(self):
+        table = "| *Date* | *Speakers* | *Notes* |\n"
+        names = self.names.copy()
+        for day in self.dates:
+            table += day.strftime("| %b %e |")
+            speaker1, speaker2, note = self.date_to_speakers(day, names)
+            if speaker1:
+                table += ' ' + speaker1
+            if speaker2:
+                table += ", " + speaker2
+            table += " | " + note + " |\n"
+        return table
+
+    # returns a string containing the currently used email layout, filled in
+    def email(self, weeks=2, today=date.today()):
+        emailstr = ""
+        names = self.names.copy()
+        i = 0
+        for day in self.dates:
+            if day <= today:
+                self.date_to_speakers(day, names)
+                continue
+            i += 1
+            if i > weeks:
+                break
+            speaker1, speaker2, note = self.date_to_speakers(day, names)
+            emailstr += day.strftime("%b %e: ")
+            if speaker1:
+                emailstr += speaker1
+                if speaker2:
+                    emailstr +=  ", " + speaker2
+                if note != "":
+                    emailstr += " (" + note + ")"
+            else:
+                emailstr += note
+            emailstr += "\n"
+        emailstr += "\nhttps://cs.uwaterloo.ca/twiki/view/CrySP/SpeakerSchedule\n"
+        return emailstr
+
+def parse_constraintsfile(filename):
+    f = open(filename, 'r')
+    tweaks = {'c': [], 'p': [], 'h': [], 'n': []}
+    for line in f:
+        l = line.rstrip().split(',')
+        tweaks[l[0]].append(l[1:])
+    return tweaks['c'], tweaks['p'], tweaks['h'], tweaks['n']
+
+def make_parser():
+    parser = argparse.ArgumentParser(description = \
+                                     "Print a speaker schedule for the CrySP weekly meetings.")
+    parser.add_argument("-s", "--start",
+                        metavar = "DATE", required = True,
+                        help = "date of the first meeting in ISO format (yyyy-mm-dd)")
+    parser.add_argument("-e", "--end", metavar = "DATE",
+                        help = "last date to schedule a meeting on or before " +\
+                        "(if -w also specified, uses whatever is shortest)")
+    parser.add_argument("-w", "--weeks", type=int,
+                        help = "number of weeks to schedule for " +\
+                        "(if -e also specified, uses whatever is shortest)")
+
+    group = parser.add_mutually_exclusive_group()
+    group.add_argument("-n", "--names",
+                       nargs = '+', default = [], metavar = "NAME",
+                       help = "names to schedule in their speaking order")
+    group.add_argument("-f", "--namesfile",
+                       metavar = "FILE",
+                       help = "path of a file that contains a whitespace " +\
+                       "and/or comma seperated list of names to schedule in " +\
+                       "their speaking order")
+
+    parser.add_argument("-c", "--constraints",
+                        metavar = ("NAME", "DATES"),
+                        nargs = '+', action = "append", default = [],
+                        help = 'specify someone *can\'t* speak on certain dates ' +\
+                        '(e.g., "-c Justin 2018-01-01 2018-01-08 -c Ian 2018-01-01")')
+    parser.add_argument("-p", "--practice",
+                        metavar = ("NAME", "DATE", "NOTE"),
+                        nargs = 3, action = "append", default = [],
+                        help = 'designate given DATE as a talk given by NAME, '+\
+                        'and remove the next instance of NAME in the list of names, '+\
+                        'if present (NOTE should usually be "practice talk")')
+    parser.add_argument("-C", "--constraintsfile",
+                        metavar = "FILE",
+                        help = 'provide constraints, practice talks, and notes ' +\
+                        'via supplied csv file')
+
+    parser.add_argument("-r", "--no-repeat",
+                        action = 'store_true',
+                        help = "disables repeating the list of names")
+    parser.add_argument("-E", "--email",
+                        nargs='?', const=2, default=False, metavar="WEEKS",
+                        help = "print an email for notifying who is speaking" +\
+                        " the next two (or specified) weeks")
+    return parser
+
+def main(argv=None):
+    if argv is None:
+        argv = sys.argv
+    parser = make_parser()
+    args = parser.parse_args()
+
+    if not args.end and not args.weeks:
+        parser.error("requires -w/--weeks or -e/--end")
+
+    if args.namesfile:
+        f = open(args.namesfile, 'r')
+        names = f.read().replace(",", " ").split()
+        f.close()
+    else:
+        names = args.names
+
+    if args.constraintsfile:
+        constraints, practice, halves, notes = parse_constraintsfile(args.constraintsfile)
+    else:
+        constraints = args.constraints
+        practice = args.practice
+
+    cal = CryspCalendar(args.start, args.end, args.weeks, names,\
+                        constraints, practice, halves, notes, args.no_repeat)
+
+    print(cal.table() if not args.email else cal.email(int(args.email)))
+    return 0;
+
+if __name__ == "__main__":
+    sys.exit(main())