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@46: Return full list of all Orders taking place during the given datetime 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@42: | (db.between(start, Order.event_start, Order.event_end))
terom@37: ).order_by(Order.event_start).all()
terom@37:
terom@46: def interval_for_dates (self, dates) :
terom@46: """
terom@46: Returns a datetime (start, end) interval encompassing all of the given dates
terom@46: """
terom@46:
terom@46: # a day is 00:00:00 - 23:59:59
terom@46: start = datetime.datetime.combine(min(dates), datetime.time(0, 0, 0))
terom@46: end = datetime.datetime.combine(max(dates), datetime.time(23, 59, 59))
terom@46:
terom@46: return start, end
terom@46:
terom@46: def hours_from_day_start (self, dt) :
terom@46: """
terom@46: Return the number of hours from the start of the day to the given datetime, as a float.
terom@46: """
terom@46:
terom@46: HOUR = float(60 * 60)
terom@46:
terom@46: # dt - 00:00:00
terom@46: return (dt - dt.replace(hour=0, minute=0, second=0)).seconds / HOUR
terom@46:
terom@46: def hours_to_day_end (self, dt) :
terom@46: """
terom@46: Return the number of hours from the given datetime to the day's end, as a float.
terom@46: """
terom@46:
terom@46: HOUR = float(60 * 60)
terom@46:
terom@46: # 23:59:59 - dt
terom@46: return (dt.replace(hour=23, minute=59, second=59) - dt).seconds / HOUR
terom@46:
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@46: # week as datetime interval
terom@46: week_start, week_end = self.interval_for_dates(week)
terom@46:
terom@46: # all orders for week
terom@37: orders = self.get_events_for_interval(week_start, week_end)
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@46: # each event on its own row for now
terom@37: for order in orders :
terom@39: # start/end date for this week
terom@46: start_day = min(date for date in week if order.on_date(date))
terom@46: end_day = max(date for date in week if order.on_date(date))
terom@39:
terom@39: # as vector into week
terom@46: leading_days = (start_day - min(week)).days
terom@46: length_days = (end_day - start_day).days + 1
terom@46: trailing_days = (max(week) - end_day).days
terom@37:
terom@46: # continues prev/next week?
terom@46: prev_week = (start_day > order.event_start.date())
terom@46: next_week = (end_day < order.event_end.date())
terom@40:
terom@46: log.debug("Event %r from %r -> %r", order.event_name, start_day, end_day)
terom@46:
terom@46: # spec class
terom@46: classes = ' '.join(cls for cls in (
terom@46: 'event',
terom@46: 'continues-prev' if prev_week else None,
terom@46: 'continues-next' if next_week else None,
terom@46: ) if cls)
terom@46:
terom@46: # compute dynamic styles
terom@46: styles = []
terom@46:
terom@46: # width of each hour in the event's span this week, as a percentage
terom@46: hour_width = 100.0 / (length_days * 24)
terom@46:
terom@46: # margin for start hour offset
terom@46: if not prev_week :
terom@46: # starts on this week, so calc how many hours into day
terom@46: styles.append('margin-left: %d%%' % int(hour_width * self.hours_from_day_start(order.event_start)))
terom@46:
terom@46: # margin for end hour offset
terom@46: if not next_week :
terom@46: # ends on this week, se calc how many hours until day end
terom@46: styles.append('margin-right: %d%%' % int(hour_width * self.hours_to_day_end(order.event_end)))
terom@46:
terom@46: # style spec
terom@46: if styles :
terom@46: style = '; '.join(styles)
terom@46:
terom@46: else :
terom@46: style = None
terom@37:
terom@39: yield tags.tr(class_='week-data')(
terom@47: ([tags.td("")] * leading_days),
terom@46: tags.td(colspan=length_days, class_=classes)(
terom@46: tags.a(href=self.url_for(urls.OrderView, id=order.id), style=style)(
terom@46: tags.div(class_='arrow-left')("") if prev_week else None,
terom@40: order.event_name,
terom@46: tags.div(class_='arrow-right')("") if next_week else None,
terom@40: )
terom@39: ),
terom@47: ([tags.td("")] * trailing_days),
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@47: # would be useful for borders, but too hard to style
terom@47: #(tags.col() for dow in cal.iterweekdays()),
terom@47:
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@54: tags.h1(u"Tilauskalenteri"),
terom@54:
terom@37: self.render_calendar(month),
terom@37: )
terom@37:
|