Browse Source

latest updates I've been using

Justin Tracey 6 months ago
parent
commit
cf22f3150c
1 changed files with 205 additions and 80 deletions
  1. 205 80
      vice-speaker-rotator.py

+ 205 - 80
vice-speaker-rotator.py

@@ -1,7 +1,9 @@
 #!/usr/bin/env python3
-from datetime import date,timedelta
-import sys, argparse
+from datetime import date, timedelta
+import sys
+import argparse
 from argparse import RawTextHelpFormatter
+import yaml
 
 # text appended to the end of generated emails
 email_body = """
@@ -12,18 +14,22 @@ Other upcoming events:
 https://cs.uwaterloo.ca/twiki/view/CrySP/UpcomingEvents
 """
 
+
 def str_to_date(string_date):
+    if type(string_date) is date:
+        return string_date
     try:
         return date(*[int(s) for s in string_date.split('-')])
     except:
         print("invalid date: " + string_date)
         sys.exit()
 
+
 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):
+                 constraints=[], practice_talks=[], half_practices=[],
+                 notes=[], irc_handles=dict(), no_repeat=False):
         self.dates = self.meeting_dates(start, end, weeks)
         self.names = names
         self.constraints = self.parse_constraints(constraints)
@@ -31,9 +37,12 @@ class CryspCalendar:
         self.practice_talks.update(self.parse_practice_talks(half_practices,
                                                              half=True))
         self.notes      = self.parse_notes(notes)
-        self.notes.update(self.parse_notes(map(lambda l:l[1:], practice_talks)))
-        self.notes.update(self.parse_notes(map(lambda l:l[1:], half_practices)))
+        self.notes.update(self.parse_notes(map(lambda l: l[1:],
+                                               practice_talks)))
+        self.notes.update(self.parse_notes(map(lambda l: l[1:],
+                                               half_practices)))
         self.verify_constraints()
+        self.irc_handles = irc_handles
         self.no_repeat = no_repeat
 
     def meeting_dates(self, start, end=None, weeks=None):
@@ -53,18 +62,19 @@ class CryspCalendar:
         return ret
 
     def parse_constraints(self, constraints):
+        """takes an iterable of (name,datestr,...) tuples"""
         constraint_dict = {}
         for constraint in constraints:
             name      = constraint[0]
             date_strs = constraint[1:]
             dates     = []
             for date_str in date_strs:
-                if '--' in date_str:
-                    #date_range = list(map(str_to_date, date_str.split('--')))
+                if type(date_str) is not date and '--' in date_str:
+                    # date_range = list(map(str_to_date, date_str.split('--')))
                     date_range = date_str.split('--')
                     date_range = [str_to_date(date_range[0]),
                                   str_to_date(date_range[1])]
-                    [dates.append(d) for d in self.dates \
+                    [dates.append(d) for d in self.dates
                      if date_range[0] <= d <= date_range[1]]
                 else:
                     dates.append(str_to_date(date_str))
@@ -72,25 +82,27 @@ class CryspCalendar:
         return constraint_dict
 
     def parse_practice_talks(self, practice, half=False):
+        """'practice' is an iterable of (name, day, note) tuples"""
         practice_dict = {}
         for talk in practice:
             name = talk[0]
-            day  = str_to_date(talk[1])
+            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,]
+                practice_dict[day] = [name, ]
             else:
                 practice_dict[day] = [name, '']
         return practice_dict
 
     def parse_notes(self, notes):
+        """takes an iterable of (day, note) tuples"""
         notes_dict = {}
         for note in notes:
-            day     = str_to_date(note[0])
+            day = str_to_date(note[0])
             message = note[1]
             assert day not in notes_dict or notes_dict[day] == message,\
                 'conflicting notes: "%s" and "%s" on %s' %\
@@ -102,7 +114,7 @@ class CryspCalendar:
         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' %\
+                    "constraint %s on %s isnt a meeting date" %\
                     (name, day)
         for day in self.practice_talks:
             assert day in self.dates,\
@@ -122,8 +134,8 @@ class CryspCalendar:
             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 \
+            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:
@@ -144,18 +156,21 @@ class CryspCalendar:
         note = self.notes.get(day, '')
         return speaker1, speaker2, note
 
-    def table(self):
+    def table(self, today=date.today()):
         """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 |')
+            color = "%SILVER% " if day <= today else ""
+            endcolor = " %ENDCOLOR%" if day <= today else ""
             speaker1, speaker2, note = self.date_to_speakers(day, names)
-            if speaker1:
-                table += ' ' + speaker1
-            if speaker2:
-                table += ', ' + speaker2
-            table += ' | ' + note + ' |\n'
+            speaker2 = ', ' + speaker2 if speaker2 else ""
+            row = "| {}{}{} | {}{}{}{} | {}{}{} |\n".format(
+                color, day.strftime('%b %e'), endcolor,
+                color, speaker1, speaker2, endcolor,
+                color, note, endcolor
+            )
+            table += row
         return table
 
     def email(self, weeks=2, today=date.today()):
@@ -175,7 +190,7 @@ class CryspCalendar:
             if speaker1:
                 emailstr += speaker1
                 if speaker2:
-                    emailstr +=  ', ' + speaker2
+                    emailstr += ', ' + speaker2
                 if note != '':
                     emailstr += ' (' + note + ')'
             else:
@@ -184,82 +199,171 @@ class CryspCalendar:
         emailstr += email_body
         return emailstr
 
+    def irc(self, today=date.today()):
+        """Returns a string for putting in the IRC channel topic"""
+        names = self.names.copy()
+        i = 0
+        for day in self.dates:
+            if day <= today:
+                self.date_to_speakers(day, names)
+                continue
+            i += 1
+            speaker1, speaker2, note = self.date_to_speakers(day, names)
+            if speaker1:
+                speaker1 = self.irc_handles.get(speaker1, speaker1)
+                if speaker2:
+                    speaker2 = ', ' + self.irc_handles.get(speaker2, speaker2)
+                if note != '':
+                    note = ' (' + note + ')'
+            return "{}{}{}".format(speaker1, speaker2, note)
+
+
 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:])
+            line = [x.strip() for x in line.rstrip().split(',')]
+            tweaks[line[0]].append(line[1:])
         return tweaks['c'], tweaks['p'], tweaks['h'], tweaks['n']
 
+
+def parse_yaml(filename, today=date.today()):
+    with open(filename, 'r') as f:
+        struct = yaml.safe_load(f)
+    currentTerm = None
+    for term in struct['terms'].values():
+        if 'start' in term and 'end' in term:
+            term['end'] = str_to_date(term['end'])
+            term['start'] = str_to_date(term['start'])
+            if currentTerm is None or (term['end'] >= today and
+                                       term['start'] > currentTerm['start']):
+                currentTerm = term
+    names = currentTerm['speakers']
+    for name in names:
+        assert name not in currentTerm['out']
+
+    def triple_check(field_name, location):
+        return field_name in location and \
+            location[field_name] is not None and \
+            len(location[field_name]) > 0
+
+    constraints = [[name] + struct['speakers'][name]['constraints']
+                   for name in names
+                   if triple_check('constraints', struct['speakers'][name])]
+    practice_talks = [(talk['name'], talk['date'], talk['note'])
+                      for talk in currentTerm['practice_talks']]
+    half_practices = [(talk['name'], talk['date'], talk['note'])
+                      for talk in currentTerm['half_practices']]
+    for speaker in struct['speakers']:
+        location = struct['speakers'][speaker]
+        if triple_check('practice_talks', location):
+            practice_talks += {(speaker, talk['date'], talk['note'])
+                               for talk in location['practice_talks']}
+        if triple_check('half_practices', location):
+            half_practices += {(speaker, talk['date'], talk['note'])
+                               for talk in location['half_practices']}
+    notes = [(note['date'], note['note']) for note in currentTerm['notes']]
+    irc_handles = {name: struct['speakers'][name]['irc']
+                   for name in struct['speakers']
+                   if 'irc' in struct['speakers'][name]}
+    return CryspCalendar(currentTerm['start'], currentTerm['end'],
+                         names=names, constraints=constraints,
+                         practice_talks=practice_talks,
+                         half_practices=half_practices,
+                         notes=notes, irc_handles=irc_handles)
+
+
 def make_parser():
-    parser = argparse.ArgumentParser(description ='''\
+    parser = argparse.ArgumentParser(description='''\
 Print a speaker schedule for the CrySP weekly meetings.
-All dates are ISO formatted (yyyy-mm-dd). Constraints in the constraints file
-can also be ranges of ISO formatted dates (yyyy-mm-dd--yyyy-mm-dd).''',
-        formatter_class=RawTextHelpFormatter)
+All dates are ISO formatted (yyyy-mm-dd). Constraints in files can also be
+ranges of ISO formatted dates (yyyy-mm-dd--yyyy-mm-dd).''',
+                                     epilog='''\
+Now that the lab is so large, you likely want to use one of the file options,
+probably YAML. The command line options still function if you want to play
+with how this script works to learn, though.
+The YAML file has the following format:
+There are two main objects, a "terms" object that stores data about particular
+terms, and a "speakers" ojbect that stores data about the speakers.
+The "terms" object contains any number of term objects with any name. Each
+term has the following objects:
+  start: the date of the first meeting
+  end: a date on and beyond which there are no more meetings
+  speakers: an ordered list of names for each speaker this term
+  out: a list to store lab members who won't speak this term (ignored)
+  practice_talks: a list of practice talks, of the form
+    {date: _, name: _, note: _}
+  half_practices: a list of half practice talks (format is the same)
+  notes: notes, of the form
+    {date: _, note: _}
+The "speakers" object contains any number of speaker objects. The name of each
+speaker object should be the same as the name in the speakers list, if they
+are speaking this term. The names can have spaces. Each speaker object has
+the following objects:
+  irc: the IRC handle of the speaker, if they're in CrySP IRC
+  email: the email address of the speaker (check the UW whitepages if need be)
+  constraints: a list of dates/date ranges the speaker cannot speak on
+  practice_talks: a list of practice talks, each of the form:
+    {date: _, note: _}
+  half_practices: a list of half practice talks (format is the same)
+''',
+                                     formatter_class=RawTextHelpFormatter)
+    parser.add_argument('-y', '--yaml',
+                        metavar='FILE',
+                        help='full description of the term via YAML file.')
     parser.add_argument('-s', '--start',
-                        metavar = 'DATE', required = True,
-                        help =
-                        'date of the first meeting')
-    parser.add_argument('-e', '--end', metavar = 'DATE',
-                        help =
-                        'last date to schedule a meeting on or before\n' \
+                        metavar='DATE',
+                        help='date of the first meeting')
+    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' \
+                        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')
+                       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')
+                       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],[date/range1],[date/range2],[...]"\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' \
+                        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],[date/range1],[date/range2],[...]"\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')
+                        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' \
+                        nargs='?', const=2, default=False,
+                        metavar='WEEKS',
+                        help='print an email for notifying who is speaking\n'
                         'the next two (or specified) weeks')
+    parser.add_argument('-i', '--irc',
+                        action='store_true',
+                        help='print names formatted for an irc channel topic')
+    parser.add_argument('-d', '--today',
+                        default=date.today(), metavar='DATE',
+                        help='pretend the script is being run on the day DATE')
     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')
 
+def old_format(args):
     if args.namesfile:
         with open(args.namesfile, 'r') as f:
             names = [x.strip() for x in f.read().split(',')]
@@ -269,15 +373,36 @@ def main(argv=None):
     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)
+        return CryspCalendar(args.start, args.end, args.weeks, names,
+                             constraints, practice, halves, notes,
+                             args.no_repeat)
+    return CryspCalendar(args.start, args.end, args.weeks, names,
+                         no_repeat=args.no_repeat)
+
+
+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 and not args.yaml:
+        parser.error('requires -w/--weeks or -e/--end')
+
+    if not args.yaml:
+        cal = old_format(args)
+    else:
+        cal = parse_yaml(args.yaml)
+
+    today = str_to_date(args.today)
+    if args.email:
+        print(cal.email(int(args.email), today))
+    elif args.irc:
+        print(cal.irc(today))
     else:
-        cal = CryspCalendar(args.start, args.end, args.weeks, names,
-                            no_repeat=args.no_repeat)
+        print(cal.table(today))
+    return 0
 
-    print(cal.table() if not args.email else cal.email(int(args.email)))
-    return 0;
 
 if __name__ == '__main__':
     sys.exit(main())