calendar.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #!/usr/bin/env python3
  2. from datetime import date,timedelta
  3. import sys
  4. import argparse
  5. def str_to_date(string_date):
  6. return date(*[int(s) for s in string_date.split('-')])
  7. class CryspCalendar:
  8. """Representation of a CrySP weekly meeting schedule for a term"""
  9. def __init__(self, start, end=None, weeks=None, names=[],\
  10. constraints=[], practice_talks=[], half_practices=[], notes=[], no_repeat=False):
  11. self.dates = self.meeting_dates(start, end, weeks)
  12. self.names = names
  13. self.constraints = self.parse_constraints(constraints)
  14. self.practice_talks = self.parse_practice_talks(practice_talks)
  15. self.practice_talks.update(self.parse_practice_talks(half_practices, half=True))
  16. self.notes = self.parse_notes(notes)
  17. self.notes.update(self.parse_notes(practice_talks))
  18. self.notes.update(self.parse_notes(half_practices))
  19. self.verify_constraints()
  20. self.no_repeat = no_repeat
  21. def meeting_dates(self, start, end=None, weeks=None):
  22. first_meeting = str_to_date(start)
  23. last_meeting = None
  24. if end:
  25. last_meeting = str_to_date(end)
  26. if weeks:
  27. last_meeting = max(first_meeting + timedelta(weeks=weeks), last_meeting)
  28. ret = []
  29. next_meeting = first_meeting
  30. one_week = timedelta(weeks=1)
  31. while next_meeting <= last_meeting:
  32. ret.append(next_meeting)
  33. next_meeting += one_week
  34. return ret
  35. def parse_constraints(self, constraints):
  36. constraint_dict = {}
  37. for constraint in constraints:
  38. name = constraint[0]
  39. date_strs = constraint[1:]
  40. dates = []
  41. for date_str in date_strs:
  42. dates.append(str_to_date(date_str))
  43. constraint_dict[name] = dates
  44. return constraint_dict
  45. def parse_practice_talks(self, practice, half=False):
  46. practice_dict = {}
  47. for talk in practice:
  48. name = talk[0]
  49. day = str_to_date(talk[1])
  50. if day in practice_dict:
  51. assert half,\
  52. "conflicting talks: %s and %s on %s" %\
  53. (practice_dict[day][0], name, day)
  54. practice_dict[day].append(name)
  55. elif half:
  56. practice_dict[day] = [name,]
  57. else:
  58. practice_dict[day] = [name, ""]
  59. return practice_dict
  60. def parse_notes(self, notes):
  61. notes_dict = {}
  62. for note in notes:
  63. day = str_to_date(note[1])
  64. message = note[2]
  65. assert day not in notes_dict or notes_dict[day] == message,\
  66. 'conflicting notes: "%s" and "%s" on %s' %\
  67. (notes_dict[day], message, note[1])
  68. notes_dict[day] = message
  69. return notes_dict
  70. def verify_constraints(self):
  71. for name in self.constraints:
  72. for day in self.constraints[name]:
  73. assert day in self.dates,\
  74. "constraint added for %s on %s, which is not a meeting date" %\
  75. (name, day)
  76. for day in self.practice_talks:
  77. assert day in self.dates,\
  78. "talk by %s designated on %s, which is not a meeting date" %\
  79. (self.practice_talks[day][0], day)
  80. for day in self.notes:
  81. assert day in self.dates,\
  82. 'note "%s" designated on %s, which is not a meeting date' %\
  83. (self.notes[day], day)
  84. def pop_name(self, day, names, i):
  85. name = None
  86. if day in self.practice_talks:
  87. if i < len(self.practice_talks[day]):
  88. name = self.practice_talks[day][i]
  89. if name is None:
  90. constrained_names = (name for name in names \
  91. if name not in self.constraints \
  92. or day not in self.constraints[name])
  93. name = next(constrained_names, None)
  94. if name and name in names:
  95. names.remove(name)
  96. return name
  97. def next_speaker(self, day, names, i):
  98. name = self.pop_name(day, names, i)
  99. if name or self.no_repeat:
  100. return name
  101. names.extend(self.names)
  102. return self.pop_name(day, names, i)
  103. def date_to_speakers(self, day, names):
  104. speaker1 = self.next_speaker(day, names, 0)
  105. speaker2 = self.next_speaker(day, names, 1)
  106. note = self.notes.get(day, "")
  107. return speaker1, speaker2, note
  108. # returns a string containing a twiki table
  109. def table(self):
  110. table = "| *Date* | *Speakers* | *Notes* |\n"
  111. names = self.names.copy()
  112. for day in self.dates:
  113. table += day.strftime("| %b %e |")
  114. speaker1, speaker2, note = self.date_to_speakers(day, names)
  115. if speaker1:
  116. table += ' ' + speaker1
  117. if speaker2:
  118. table += ", " + speaker2
  119. table += " | " + note + " |\n"
  120. return table
  121. # returns a string containing the currently used email layout, filled in
  122. def email(self, weeks=2, today=date.today()):
  123. emailstr = ""
  124. names = self.names.copy()
  125. i = 0
  126. for day in self.dates:
  127. if day <= today:
  128. self.date_to_speakers(day, names)
  129. continue
  130. i += 1
  131. if i > weeks:
  132. break
  133. speaker1, speaker2, note = self.date_to_speakers(day, names)
  134. emailstr += day.strftime("%b %e: ")
  135. if speaker1:
  136. emailstr += speaker1
  137. if speaker2:
  138. emailstr += ", " + speaker2
  139. if note != "":
  140. emailstr += " (" + note + ")"
  141. else:
  142. emailstr += note
  143. emailstr += "\n"
  144. emailstr += "\nhttps://cs.uwaterloo.ca/twiki/view/CrySP/SpeakerSchedule\n"
  145. return emailstr
  146. def parse_constraintsfile(filename):
  147. f = open(filename, 'r')
  148. tweaks = {'c': [], 'p': [], 'h': [], 'n': []}
  149. for line in f:
  150. l = [x.strip() for x in line.rstrip().split(',')]
  151. tweaks[l[0]].append(l[1:])
  152. return tweaks['c'], tweaks['p'], tweaks['h'], tweaks['n']
  153. def make_parser():
  154. parser = argparse.ArgumentParser(description = \
  155. "Print a speaker schedule for the CrySP weekly meetings.")
  156. parser.add_argument("-s", "--start",
  157. metavar = "DATE", required = True,
  158. help = "date of the first meeting in ISO format (yyyy-mm-dd)")
  159. parser.add_argument("-e", "--end", metavar = "DATE",
  160. help = "last date to schedule a meeting on or before " +\
  161. "(if -w also specified, uses whatever is shortest)")
  162. parser.add_argument("-w", "--weeks", type=int,
  163. help = "number of weeks to schedule for " +\
  164. "(if -e also specified, uses whatever is shortest)")
  165. group = parser.add_mutually_exclusive_group()
  166. group.add_argument("-n", "--names",
  167. nargs = '+', default = [], metavar = "NAME",
  168. help = "names to schedule in their speaking order")
  169. group.add_argument("-f", "--namesfile",
  170. metavar = "FILE",
  171. help = "path of a file that contains a whitespace " +\
  172. "and/or comma seperated list of names to schedule in " +\
  173. "their speaking order")
  174. parser.add_argument("-c", "--constraints",
  175. metavar = ("NAME", "DATES"),
  176. nargs = '+', action = "append", default = [],
  177. help = 'specify someone *can\'t* speak on certain dates ' +\
  178. '(e.g., "-c Justin 2018-01-01 2018-01-08 -c Ian 2018-01-01")')
  179. parser.add_argument("-p", "--practice",
  180. metavar = ("NAME", "DATE", "NOTE"),
  181. nargs = 3, action = "append", default = [],
  182. help = 'designate given DATE as a talk given by NAME, '+\
  183. 'and remove the next instance of NAME in the list of names, '+\
  184. 'if present (NOTE should usually be "practice talk")')
  185. parser.add_argument("-C", "--constraintsfile",
  186. metavar = "FILE",
  187. help = 'provide constraints, practice talks, and notes ' +\
  188. 'via supplied csv file')
  189. parser.add_argument("-r", "--no-repeat",
  190. action = 'store_true',
  191. help = "disables repeating the list of names")
  192. parser.add_argument("-E", "--email",
  193. nargs='?', const=2, default=False, metavar="WEEKS",
  194. help = "print an email for notifying who is speaking" +\
  195. " the next two (or specified) weeks")
  196. return parser
  197. def main(argv=None):
  198. if argv is None:
  199. argv = sys.argv
  200. parser = make_parser()
  201. args = parser.parse_args()
  202. if not args.end and not args.weeks:
  203. parser.error("requires -w/--weeks or -e/--end")
  204. if args.namesfile:
  205. f = open(args.namesfile, 'r')
  206. names = [x.strip() for x in f.read().split(',')]
  207. f.close()
  208. else:
  209. names = args.names
  210. if args.constraintsfile:
  211. constraints, practice, halves, notes = parse_constraintsfile(args.constraintsfile)
  212. else:
  213. constraints = args.constraints
  214. practice = args.practice
  215. cal = CryspCalendar(args.start, args.end, args.weeks, names,\
  216. constraints, practice, halves, notes, args.no_repeat)
  217. print(cal.table() if not args.email else cal.email(int(args.email)))
  218. return 0;
  219. if __name__ == "__main__":
  220. sys.exit(main())