123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- #!/usr/bin/env python3
- from datetime import date,timedelta
- import sys, argparse
- from argparse import RawTextHelpFormatter
- # text appended to the end of generated emails
- email_body = """
- Full speaker schedule:
- https://cs.uwaterloo.ca/twiki/view/CrySP/SpeakerSchedule
- Other upcoming events:
- https://cs.uwaterloo.ca/twiki/view/CrySP/UpcomingEvents
- """
- 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):
- """Pops the name of the next available on day from the names stack,
- where 'i' is which speaker they would be for that day (0 or 1).
- Returns None if no speakers are left in the given names stack."""
- 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):
- """Wrapper for pop_name() that repopulates names stack if needed."""
- 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
- def table(self):
- """Returns a string containing a twiki table."""
- 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
- def email(self, weeks=2, today=date.today()):
- """Returns a string for emailing to the lab who is speaking next"""
- 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 += email_body
- return emailstr
- def parse_constraintsfile(filename):
- with open(filename, 'r') as f:
- 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.',
- formatter_class=RawTextHelpFormatter)
- parser.add_argument('-s', '--start',
- metavar = 'DATE', required = True,
- help =
- 'date of the first meeting in ISO format\n' +\
- '(yyyy-mm-dd)')
- parser.add_argument('-e', '--end', metavar = 'DATE',
- help =
- 'last date to schedule a meeting on or before\n' +\
- '(if -w also specified, uses whatever is shortest)')
- parser.add_argument('-w', '--weeks', type=int,
- help =
- 'number of weeks to schedule for\n' +\
- '(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 comma-seperated\n' +\
- ' list of names to schedule in their speaking order')
- parser.add_argument('-c', '--constraintsfile',
- metavar = 'FILE',
- help =
- 'Provide constraints, practice talks, and notes\n' +\
- 'via supplied csv file. The CSV can contain the\n' +\
- 'following lines:\n' +\
- 'constraints - dates where someone cannot speak:\n' +\
- '"c,[name],[date1],[date2],[...]"\n' +\
- 'notes - goes in the notes column for that date:\n' +\
- '"n,[date],[note]"\n' +\
- 'practice talks - dates where something other than\n' +\
- 'the usual CrySP meeting talks is happening:\n' +\
- '"p,[name],[date],[note (e.g., "practice talk")]"\n' +\
- 'half-slot - a rare case where someone/thing needs\n' +\
- 'only one of the speaker slots on this date:\n' +\
- '"h,[name],[date],[note]"\n')
- 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\n' +\
- '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:
- with open(args.namesfile, 'r') as f:
- names = [x.strip() for x in f.read().split(',')]
- else:
- names = args.names
- if args.constraintsfile:
- constraints, practice, halves, notes = \
- parse_constraintsfile(args.constraintsfile)
- cal = CryspCalendar(args.start, args.end, args.weeks, names,
- constraints, practice, halves, notes,
- args.no_repeat)
- else:
- cal = CryspCalendar(args.start, args.end, args.weeks, names,
- no_repeat=args.no_repeat)
- print(cal.table() if not args.email else cal.email(int(args.email)))
- return 0;
- if __name__ == '__main__':
- sys.exit(main())
|