terom@37: """ terom@37: Calendar view of orders terom@37: """ terom@37: terom@37: from svv.controllers import PageHandler terom@37: from svv.html import tags terom@37: from svv import database as db terom@37: from svv import html terom@37: terom@37: terom@37: import datetime terom@37: import calendar terom@37: import logging terom@37: terom@37: log = logging.getLogger('svv.cal') terom@37: terom@37: class CalendarView (PageHandler) : terom@37: """ terom@37: Single-month calendar view with events for given month shown terom@37: """ terom@37: terom@37: # first date of week terom@41: FIRST_WEEKDAY = 0 terom@37: terom@37: # year/month format for URLs terom@37: URL_FORMAT = "%Y-%m" terom@37: terom@37: # formatting styles terom@37: MONTH_TITLE_FORMAT = "%B %Y" terom@37: terom@37: @classmethod terom@37: def dayofweek_title (cls, dow) : terom@37: """ terom@37: Return day of week name for given dow number terom@37: """ terom@37: terom@37: return calendar.day_name[dow] terom@37: terom@37: @classmethod terom@37: def _wrap_year (cls, year, month) : terom@37: """ terom@37: Wraps month to between [1, 12], spilling overflow/underflow by to year. terom@37: terom@37: Returns (year, month) terom@37: """ terom@37: terom@37: # underflow? terom@37: if month == 0 : terom@37: # wrap to previous year terom@37: return (year - 1, 12) terom@37: terom@37: # overflow? terom@37: elif month == 13 : terom@37: # wrap to next year terom@37: return (year + 1, 1) terom@37: terom@37: # sane value terom@37: elif 1 <= month <= 12 : terom@37: return (year, month) terom@37: terom@37: # insane value terom@37: else : terom@37: assert False, "invalid year/month: %d/%d" % (year, month) terom@37: terom@37: @classmethod terom@37: def prev_month (cls, month) : terom@37: """ terom@37: Compute date for previous month. terom@37: """ terom@37: terom@37: # normalize terom@37: y, m = cls._wrap_year(month.year, month.month - 1) terom@37: terom@37: return datetime.date(y, m, 1) terom@37: terom@37: @classmethod terom@37: def next_month (cls, month) : terom@37: """ terom@37: Compute date for following month. terom@37: """ terom@37: terom@37: # normalize terom@37: y, m = cls._wrap_year(month.year, month.month + 1) terom@37: terom@37: return datetime.date(y, m, 1) terom@37: terom@37: terom@37: def process (self, **foo) : terom@37: """ terom@37: Setup terom@37: """ terom@37: terom@37: # db session terom@37: self.session = self.app.session() terom@37: terom@37: def render_day_header (self, month, date) : terom@37: """ terom@37: Render for day terom@37: """ terom@37: terom@37: today = datetime.date.today() terom@37: terom@37: classes = [] terom@37: terom@37: if (date.year, date.month) == (month.year, month.month) : terom@37: # current month terom@37: classes.append('in-month') terom@37: terom@37: else : terom@37: classes.append('out-month') terom@37: terom@37: if date == today : terom@37: classes.append('today') terom@37: terom@37: class_ = ' '.join(classes) terom@37: terom@37: return tags.th(date.day, class_=class_) terom@37: terom@37: def get_events_for_interval (self, start, end) : terom@37: """ terom@37: Returns list of Order objects for given interval, ordered by start time. terom@37: """ terom@37: terom@37: # XXX: bad imports terom@37: from orders import Order terom@37: terom@37: return self.session.query(Order).filter( terom@37: (Order.event_start.between(start, end)) terom@37: | (Order.event_end.between(start, end)) terom@37: ).order_by(Order.event_start).all() terom@37: terom@37: def render_week (self, month, week) : terom@37: """ terom@37: Render day rows for given week. terom@37: """ terom@37: terom@37: # XXX: nasty terom@37: from svv import urls terom@37: terom@37: # load events for week terom@37: week_start = datetime.datetime.combine(min(week), datetime.time(0, 0, 0)) terom@37: week_end = datetime.datetime.combine(max(week), datetime.time(23, 59, 59)) terom@37: terom@37: orders = self.get_events_for_interval(week_start, week_end) terom@37: terom@37: log.debug("Render week %r -> %r: %d", week_start, week_end, len(orders)) terom@37: terom@37: # day headers terom@37: yield tags.tr(class_='week-header')( terom@37: self.render_day_header(month, date) for date in week terom@37: ) terom@37: terom@37: # each even on its own row for now terom@37: for order in orders : terom@39: # start/end date for this week terom@39: start = min(date for date in week if order.on_date(date)) terom@39: end = max(date for date in week if order.on_date(date)) terom@39: terom@39: # as vector into week terom@39: leading = (start - min(week)).days terom@39: length = (end - start).days + 1 terom@39: trailing = (max(week) - end).days terom@37: terom@40: # continues prev/next? terom@40: prev = (start > order.event_start.date()) terom@40: next = (end < order.event_end.date()) terom@40: terom@39: log.debug("Event %r from %r -> %r", order.event_name, start, end) terom@37: terom@39: yield tags.tr(class_='week-data')( terom@39: [tags.td("")] * leading, terom@40: tags.td(colspan=length, class_=(' '.join(cls for cls in ( terom@40: 'event', terom@40: 'continues-prev' if prev else None, terom@40: 'continues-next' if next else None, terom@40: ) if cls)))( terom@40: tags.a(href=self.url_for(urls.OrderView, id=order.id))( terom@40: tags.div(class_='arrow-left')("") if prev else None, terom@40: order.event_name, terom@40: tags.div(class_='arrow-right')("") if next else None, terom@40: ) terom@39: ), terom@39: [tags.td("")] * trailing, terom@37: ) terom@37: terom@37: def render_calendar (self, month) : terom@37: """ terom@37: Render calendar for given date's month. terom@37: """ terom@37: terom@37: cal = calendar.Calendar(self.FIRST_WEEKDAY) terom@37: terom@37: # next/prev month terom@37: prev = self.prev_month(month) terom@37: next = self.next_month(month) terom@37: terom@37: return tags.table(class_='calendar')( terom@37: tags.caption( terom@37: tags.a(href=self.url_for(CalendarView, yearmonth=prev.strftime(self.URL_FORMAT)), class_='prev-month')( terom@37: html.raw("«") terom@37: ), terom@37: month.strftime(self.MONTH_TITLE_FORMAT), terom@37: tags.a(href=self.url_for(CalendarView, yearmonth=next.strftime(self.URL_FORMAT)), class_='next-month')( terom@37: html.raw("»") terom@37: ), terom@37: ), terom@37: terom@37: # week-day headers terom@37: tags.thead( terom@37: tags.tr( terom@37: tags.th( terom@37: self.dayofweek_title(dow) terom@37: ) for dow in cal.iterweekdays() terom@37: ) terom@37: ), terom@37: terom@37: # month weeks terom@37: tags.tbody( terom@37: ( terom@37: self.render_week(month, week) terom@37: ) for week in cal.monthdatescalendar(month.year, month.month) terom@37: ), terom@37: ) terom@37: terom@37: def render_content (self, yearmonth=None) : terom@37: """ terom@37: Render calendar HTML for given year/month. terom@37: """ terom@37: terom@37: if yearmonth : terom@37: # requested month terom@37: month = datetime.datetime.strptime(yearmonth, self.URL_FORMAT).date() terom@37: terom@37: else : terom@37: # this month terom@37: month = datetime.date.today() terom@37: terom@37: return ( terom@37: self.render_calendar(month), terom@37: ) terom@37: