svv/cal.py
author Tero Marttila <terom@fixme.fi>
Sat, 08 Jan 2011 23:52:47 +0200
changeset 41 36d029a47d37
parent 40 30a0a0fa8c54
child 42 fa5694ee0f98
permissions -rw-r--r--
cal: week starts on monday
"""
    Calendar view of orders
"""

from svv.controllers import PageHandler
from svv.html import tags
from svv import database as db
from svv import html


import datetime
import calendar
import logging

log = logging.getLogger('svv.cal')

class CalendarView (PageHandler) :
    """
        Single-month calendar view with events for given month shown
    """

    # first date of week
    FIRST_WEEKDAY = 0
    
    # year/month format for URLs
    URL_FORMAT = "%Y-%m"

    # formatting styles
    MONTH_TITLE_FORMAT = "%B %Y"

    @classmethod
    def dayofweek_title (cls, dow) :
        """
            Return day of week name for given dow number
        """

        return calendar.day_name[dow]

    @classmethod
    def _wrap_year (cls, year, month) :
        """
            Wraps month to between [1, 12], spilling overflow/underflow by to year.

            Returns (year, month)
        """

        # underflow?
        if month == 0 :
            # wrap to previous year
            return (year - 1, 12)

        # overflow?
        elif month == 13 :
            # wrap to next year
            return (year + 1, 1)

        # sane value
        elif 1 <= month <= 12 :
            return (year, month)

        # insane value
        else :
            assert False, "invalid year/month: %d/%d" % (year, month)
    
    @classmethod
    def prev_month (cls, month) :
        """
            Compute date for previous month.
        """

        # normalize
        y, m = cls._wrap_year(month.year, month.month - 1)

        return datetime.date(y, m, 1)

    @classmethod
    def next_month (cls, month) :
        """
            Compute date for following month.
        """

        # normalize
        y, m = cls._wrap_year(month.year, month.month + 1)

        return datetime.date(y, m, 1)


    def process (self, **foo) :
        """
            Setup
        """

        # db session
        self.session = self.app.session()

    def render_day_header (self, month, date) :
        """
            Render <th> for day
        """

        today = datetime.date.today()

        classes = []

        if (date.year, date.month) == (month.year, month.month) :
            # current month
            classes.append('in-month')

        else :
            classes.append('out-month')

        if date == today :
            classes.append('today')

        class_ = ' '.join(classes)

        return tags.th(date.day, class_=class_)

    def get_events_for_interval (self, start, end) :
        """
            Returns list of Order objects for given interval, ordered by start time.
        """

        # XXX: bad imports
        from orders import Order

        return self.session.query(Order).filter(
                (Order.event_start.between(start, end))
            |   (Order.event_end.between(start, end))
        ).order_by(Order.event_start).all()

    def render_week (self, month, week) :
        """
            Render day rows for given week.
        """
        
        # XXX: nasty
        from svv import urls

        # load events for week
        week_start = datetime.datetime.combine(min(week), datetime.time(0, 0, 0))
        week_end = datetime.datetime.combine(max(week), datetime.time(23, 59, 59))

        orders = self.get_events_for_interval(week_start, week_end)

        log.debug("Render week %r -> %r: %d", week_start, week_end, len(orders))
        
        # day headers
        yield tags.tr(class_='week-header')(
            self.render_day_header(month, date) for date in week
        )

        # each even on its own row for now
        for order in orders :
            # start/end date for this week
            start = min(date for date in week if order.on_date(date))
            end = max(date for date in week if order.on_date(date))
            
            # as vector into week
            leading = (start - min(week)).days
            length = (end - start).days + 1
            trailing = (max(week) - end).days

            # continues prev/next?
            prev = (start > order.event_start.date())
            next = (end < order.event_end.date())

            log.debug("Event %r from %r -> %r", order.event_name, start, end)

            yield tags.tr(class_='week-data')(
                [tags.td("")] * leading,
                tags.td(colspan=length, class_=(' '.join(cls for cls in (
                    'event',
                    'continues-prev' if prev else None,
                    'continues-next' if next else None,
                ) if cls)))(
                    tags.a(href=self.url_for(urls.OrderView, id=order.id))(
                        tags.div(class_='arrow-left')("") if prev else None,
                        order.event_name,
                        tags.div(class_='arrow-right')("") if next else None,
                    )
                ),
                [tags.td("")] * trailing,
            )

    def render_calendar (self, month) :
        """
            Render calendar for given date's month.
        """

        cal = calendar.Calendar(self.FIRST_WEEKDAY)

        # next/prev month
        prev = self.prev_month(month)
        next = self.next_month(month)

        return tags.table(class_='calendar')(
            tags.caption(
                tags.a(href=self.url_for(CalendarView, yearmonth=prev.strftime(self.URL_FORMAT)), class_='prev-month')(
                    html.raw("&laquo;")
                ),
                month.strftime(self.MONTH_TITLE_FORMAT),
                tags.a(href=self.url_for(CalendarView, yearmonth=next.strftime(self.URL_FORMAT)), class_='next-month')(
                    html.raw("&raquo;")
                ),
            ),
            
            # week-day headers
            tags.thead(
                tags.tr(
                    tags.th(
                        self.dayofweek_title(dow)
                    ) for dow in cal.iterweekdays()
                )
            ),

            # month weeks
            tags.tbody(
                (
                    self.render_week(month, week)
                ) for week in cal.monthdatescalendar(month.year, month.month)
            ),
        )

    def render_content (self, yearmonth=None) :
        """
            Render calendar HTML for given year/month.
        """

        if yearmonth :
            # requested month
            month = datetime.datetime.strptime(yearmonth, self.URL_FORMAT).date()

        else :
            # this month
            month = datetime.date.today()
        
        return (
            self.render_calendar(month),
        )