Newer
Older
Handbook / calendar / teachingdates / calendars / basecalendar.py
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.lecture_offsets = []
        self.skipped_lectures = []
        if self.config.paper:
            self.update_lectures(self.config.get_paper_config(self.config.paper))

    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_lectures(self, lecture_list):
        for l in lecture_list["lectures"]:
            for t, v in l.items():
                if t == "offsets":
                    self.lecture_offsets = v
                elif t == "skip":
                    self.skipped_lectures = v
                else:
                    print("{prog}: warning: ignored unknown lecture key "
                          "'{key}'.".format(prog=PROG, 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 lecture_dates(self):
        dates = {}
        lecture_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.lecture_offsets):
                lec = week_index * 2 + offset_index + 1
                if lec in self.skipped_lectures:
                    continue
                dates[lecture_num] = monday + datetime.timedelta(offset)
                lecture_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 == "lecture":
            weeks = self.lecture_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