123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- #!/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())
|