"""
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("«")
),
month.strftime(self.MONTH_TITLE_FORMAT),
tags.a(href=self.url_for(CalendarView, yearmonth=next.strftime(self.URL_FORMAT)), class_='next-month')(
html.raw("»")
),
),
# 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),
)