svv/cal.py
author Tero Marttila <terom@fixme.fi>
Mon, 10 Jan 2011 18:30:58 +0200
changeset 54 d077f2f60098
parent 47 d79a560af791
permissions -rw-r--r--
cal: page title
"""
    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) :
        """
            Return full list of all Orders taking place during the given datetime 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))
            |   (db.between(start, Order.event_start, Order.event_end))
        ).order_by(Order.event_start).all()

    def interval_for_dates (self, dates) :
        """
            Returns a datetime (start, end) interval encompassing all of the given dates
        """
        
        # a day is 00:00:00 - 23:59:59
        start = datetime.datetime.combine(min(dates), datetime.time(0, 0, 0))
        end = datetime.datetime.combine(max(dates), datetime.time(23, 59, 59))
        
        return start, end

    def hours_from_day_start (self, dt) :
        """
            Return the number of hours from the start of the day to the given datetime, as a float.
        """

        HOUR = float(60 * 60)

        # dt - 00:00:00
        return (dt - dt.replace(hour=0, minute=0, second=0)).seconds / HOUR

    def hours_to_day_end (self, dt) :
        """
            Return the number of hours from the given datetime to the day's end, as a float.
        """

        HOUR = float(60 * 60)
        
        # 23:59:59 - dt
        return (dt.replace(hour=23, minute=59, second=59) - dt).seconds / HOUR

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

        # week as datetime interval
        week_start, week_end = self.interval_for_dates(week)
        
        # all orders for week
        orders = self.get_events_for_interval(week_start, week_end)

        # day headers
        yield tags.tr(class_='week-header')(
            self.render_day_header(month, date) for date in week
        )

        # each event on its own row for now
        for order in orders :
            # start/end date for this week
            start_day = min(date for date in week if order.on_date(date))
            end_day = max(date for date in week if order.on_date(date))
            
            # as vector into week
            leading_days = (start_day - min(week)).days
            length_days = (end_day - start_day).days + 1
            trailing_days = (max(week) - end_day).days

            # continues prev/next week?
            prev_week = (start_day > order.event_start.date())
            next_week = (end_day < order.event_end.date())

            log.debug("Event %r from %r -> %r", order.event_name, start_day, end_day)

            # spec class
            classes = ' '.join(cls for cls in (
                'event',
                'continues-prev' if prev_week else None,
                'continues-next' if next_week else None,
            ) if cls)

            # compute dynamic styles
            styles = []

            # width of each hour in the event's span this week, as a percentage
            hour_width = 100.0 / (length_days * 24)

            # margin for start hour offset
            if not prev_week :
                # starts on this week, so calc how many hours into day
                styles.append('margin-left: %d%%' % int(hour_width * self.hours_from_day_start(order.event_start)))
            
            # margin for end hour offset
            if not next_week :
                # ends on this week, se calc how many hours until day end
                styles.append('margin-right: %d%%' % int(hour_width * self.hours_to_day_end(order.event_end)))
            
            # style spec
            if styles :
                style = '; '.join(styles)

            else :
                style = None

            yield tags.tr(class_='week-data')(
                ([tags.td("")] * leading_days),
                tags.td(colspan=length_days, class_=classes)(
                    tags.a(href=self.url_for(urls.OrderView, id=order.id), style=style)(
                        tags.div(class_='arrow-left')("") if prev_week else None,
                        order.event_name,
                        tags.div(class_='arrow-right')("") if next_week else None,
                    )
                ),
                ([tags.td("")] * trailing_days),
            )

    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;")
                ),
            ),
            
            # would be useful for borders, but too hard to style
            #(tags.col() for dow in cal.iterweekdays()),
            
            # 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 (
            tags.h1(u"Tilauskalenteri"),

            self.render_calendar(month),
        )