#!/usr/bin/env python3 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]) if day in practice_dict: assert half,\ "conflicting talks: %s and %s on %s" %\ (practice_dict[day][0], name, day) practice_dict[day].append(name) elif 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 or notes_dict[day] == message,\ '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_name(self, day, names, i): name = None if day in self.practice_talks: if i < len(self.practice_talks[day]): name = self.practice_talks[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 = [x.strip() for x in 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 = [x.strip() for x in f.read().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())