calendar.py 9.7 KB

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