import calendar import datetime import jinja2 from num2words import num2words from teachingdates import PROG from .errors import TemplateStyleError from .weeks import BreakWeek, IsoWeek, TeachingWeek suffix_map = { "latex": "tex", "text": "txt", "xml": "xml", } filters = { "isteaching": lambda w: isinstance(w, TeachingWeek), "isbreak": lambda w: isinstance(w, BreakWeek), "pad": lambda s, n: str(s).rjust(n), "num2words": lambda s: num2words(str(s)).title().replace("-", "") } class TeachingCalendar(): """This class generates teaching-related dates for a specific paper offered in a specific teaching period of a specific year. If you don't provide teaching period details, it generates dates for the current ISO-8601 year. """ def __init__(self, config): self.config = config cal = calendar.Calendar().yeardatescalendar(config.year, width=12) # Extract the Mondays at the start of each week. Because Calendar # is designed to produce data suitable for a printed calendar, # it includes all weeks that overlap with the calendar year, # regardless of whether they're actually part of the ISO year. # We therefore also need to filter on the current ISO year. self.mondays = [ week[0] for month in cal[0] for week in month if week[0].isocalendar()[0] == config.year ] # Similarly, each month generated by Calendar includes all days # from the first and last weeks of the month, which may be from # the preceding or succeeding month, respectively. These show # up as duplicates that need to be removed. self.mondays = list(dict.fromkeys(self.mondays)) self.period_weeks = {} self.calendars = {} for period in self.config.get_config_keys(): self.period_weeks[period] = self.generate_period_weeks( self.config.get_period_config(period)["weeks"]) self.calendars[period] = self.generate_calendar(period) self.period_weeks["iso"] = self.generate_period_weeks( [{"iso": [ self.mondays[0].isocalendar()[1], # first ISO week self.mondays[-1].isocalendar()[1] # last ISO week ]}]) self.calendars["iso"] = self.generate_calendar("iso") self.class_offsets = [] self.skipped_classes = [] if self.config.paper: self.update_classes( self.config.get_paper_config(self.config.paper), class_type=self.config.style + "s" ) def make_week(self, week_type, start, end=None): end = start if end is None else end return [ week_type( self.mondays[w].year, self.mondays[w].month, self.mondays[w].day) for w in range(start - 1, end)] def generate_period_weeks(self, week_list): weeks = [] for w in week_list: for t, r in w.items(): if t == "teach": weeks += self.make_week(TeachingWeek, r[0], r[1]) elif t == "break": weeks += self.make_week(BreakWeek, r[0], r[1]) elif t == "iso": weeks += self.make_week(IsoWeek, r[0], r[1]) else: print("{prog}: warning: ignored unknown week type " "'{type}'.".format(prog=PROG, type=t)) return weeks def update_classes(self, class_list, class_type="lectures"): for l in class_list[class_type]: for t, v in l.items(): if t == "offsets": self.class_offsets = v elif t == "skip": self.skipped_classes = v else: print("{prog}: warning: ignored unknown {type} key " "'{key}'.".format( prog=PROG, type=class_type[:-1], key=t)) # turn this into a generator? def generate_calendar(self, period): result = {} week_num = 0 break_num = 0 for w in self.period_weeks[period]: if isinstance(w, (TeachingWeek, IsoWeek)): # Increment first so that break weeks have the # same week number as the preceding teaching week. # This ensures the keys are always chronologically # sorted. Reset the break number for each new # teaching week. week_num += 1 break_num = 0 result[week_num] = w # if period == "ISO": # print("week ", week_num, " ", w) elif isinstance(w, BreakWeek): # Allow for up to 99 consecutive break weeks, # which should be sufficient :). Should probably # throw an exception if we exceed that. break_num += 0.01 result[week_num + break_num] = w return result # turn this into a generator? def class_dates(self): dates = {} class_num = 1 teaching_weeks = [t for t in self.calendars[self.config.period].values() if isinstance(t, TeachingWeek)] for week_index, monday in enumerate(teaching_weeks): for offset_index, offset in enumerate(self.class_offsets): cls = week_index * 2 + offset_index + 1 if cls in self.skipped_classes: continue dates[class_num] = monday + datetime.timedelta(offset) class_num += 1 return dates def render_latex(self, weeks): env = jinja2.Environment( block_start_string="\\BLOCK{", block_end_string="}", variable_start_string="\\VAR{", variable_end_string="}", comment_start_string="\\#{", comment_end_string="}", line_statement_prefix="%%", line_comment_prefix="%#", trim_blocks=True, autoescape=False, loader=jinja2.PackageLoader("teachingdates", "templates")) env.filters.update(filters) template = env.get_template( "{style}.tex.j2".format(style=self.config.style)) return template.render( weeks=weeks, paper=self.config.paper, period=self.config.period, year=self.config.year, eow_offset=datetime.timedelta(self.config.end_of_week)) def render_text(self, weeks): env = jinja2.Environment( loader=jinja2.PackageLoader("teachingdates", "templates"), autoescape=False) env.filters.update(filters) template = env.get_template( "{style}.txt.j2".format(style=self.config.style)) return template.render( weeks=weeks, paper=self.config.paper, period=self.config.period, year=self.config.year, eow_offset=datetime.timedelta(self.config.end_of_week)) def render_xml(self): env = jinja2.Environment( loader=jinja2.PackageLoader("teachingdates", "templates"), autoescape=jinja2.select_autoescape(["xml"])) env.filters.update(filters) template = env.get_template("paper-calendar-dates.xml.j2") return template.render( calendars=self.calendars, eow_offset=datetime.timedelta(self.config.end_of_week)) def render(self, style, fmt): if self.config.style in ["lecture", "lab"]: weeks = self.class_dates() else: weeks = self.calendars[self.config.period] if fmt == "latex": return(self.render_latex(weeks)) elif fmt == "text": return(self.render_text(weeks)) elif fmt == "xml": return(self.render_xml()) else: return None