Newer
Older
Handbook / calendar / teachingdates / calendars / basecalendar.py
import calendar
import datetime

import jinja2

from teachingdates import PROG

from .errors import TemplateStyleError
from .weeks import BreakWeek, IsoWeek, TeachingWeek

suffix_map = {
    "latex": "tex",
    "text": "txt",
    "xml": "xml",
}


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.
    """
    weeks = []
    lecture_offsets = []
    skipped_lectures = []
    mondays = []
    period = None
    paper = None
    end_of_week = None
    year = datetime.date.today().year
    env = jinja2.Environment(
        loader=jinja2.PackageLoader("teachingdates", "templates"),
        autoescape=jinja2.select_autoescape(["html", "xml"]))

    def __init__(self,
                 year,
                 period,
                 paper,
                 end_of_week=None,
                 period_config=None,
                 paper_config=None):
        self.year = year
        self.period = period
        self.paper = paper
        self.end_of_week = end_of_week

        self.env.filters.update({
            "isteaching": lambda w: isinstance(w, TeachingWeek),
            "isbreak": lambda w: isinstance(w, BreakWeek),
            "eow": lambda d: d + datetime.timedelta(days=self.end_of_week),
            "pad": lambda s, n: str(s).rjust(n)
        })

        # This is likely to end up with one week too many at the end,
        # but that doesn't matter as there will never be any teaching
        # during the last week of December!
        cal = calendar.Calendar().yeardatescalendar(self.year, width=12)

        self.mondays = [week[0] for month in cal[0] for week in month]
        self.mondays = list(dict.fromkeys(self.mondays))
        if period_config:
            self.update_weeks(period_config["weeks"])
        else:
            last_week = datetime.date(self.year, 12, 28).isocalendar()[1]
            self.update_weeks([{"iso": [1, last_week]}])
        if paper_config:
            self.update_lectures(paper_config["lectures"])

    @classmethod
    def from_configuration(cls, config):
        if config:
            return cls(config.year, config.period, config.paper,
                       config.end_of_week, config.get_period_config(),
                       config.get_paper_config())
        else:
            return None

    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 teaching_date(self, period, week, offset=0):
        pass

    def update_weeks(self, week_list):
        for w in week_list:
            for t, r in w.items():
                if t == "teach":
                    self.weeks += self.make_week(TeachingWeek, r[0], r[1])
                elif t == "break":
                    self.weeks += self.make_week(BreakWeek, r[0], r[1])
                elif t == "iso":
                    self.weeks += self.make_week(IsoWeek, r[0], r[1])
                else:
                    print("{prog}: warning: ignored unknown week type "
                          "'{type}'.".format(prog=PROG, type=t))

    def update_lectures(self, lecture_list):
        for l in lecture_list:
            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 calendar(self):
        period = {}
        week_num = break_num = 0
        for w in self.weeks:
            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
                period[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
                period[week_num + break_num] = w
        return period

    # turn this into a generator?
    def lecture_dates(self):
        dates = {}
        lecture_num = 1
        teaching_weeks = [t for t in self.weeks 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(self, style, fmt):
        suffix = suffix_map[fmt]
        template = self.env.get_template("{style}.{suffix}.j2".format(
            style=style, suffix=suffix))
        if style in ["calendar", "iso"]:
            data = self.calendar()
        elif style == "lecture":
            data = self.lecture_dates()
        else:
            pass
        return template.render(period=self.period,
                               year=self.year,
                               paper=self.paper,
                               data=data)