cal: simple order calendar view
authorTero Marttila <terom@fixme.fi>
Sat, 08 Jan 2011 22:52:25 +0200
changeset 37 eabea2857143
parent 36 d7a159024912
child 38 673475e05e3d
cal: simple order calendar view
static/cal.css
svv/cal.py
svv/controllers.py
svv/orders.py
svv/urls.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/static/cal.css	Sat Jan 08 22:52:25 2011 +0200
@@ -0,0 +1,71 @@
+/* The calendar uses up all available horizontal width */
+table.calendar
+{
+    width: 100%;
+}
+
+/* Each column in the table is approximately the same width */
+table.calendar th
+{
+    width: 14%;
+}
+
+/* The prev-month link is on the left edge */
+table.calendar a.prev-month
+{
+    float: left;
+}
+
+/* The next-month link is on the right edge */
+table.calendar a.next-month
+{
+    float: right;
+}
+
+/* The weekdays-in-week header is fixed-height */
+table.calendar thead tr
+{
+    height: 1em;
+}
+
+/* A day's header is a fixed height cell */
+table.calendar tbody tr.week-header
+{    
+    height: 1em;
+}
+
+/* The day number is visible inside the header */
+table.calendar tbody tr.week-header th
+{
+    padding-left: 0.5em;
+
+    text-align: left;
+}
+
+/* The numbers of days that are a part of the current month are clearly visible */
+table.calendar tbody th.in-month
+{
+    background-color: #eeeeee;
+}
+
+/* The numbers of days that are outside the current month are less noticable */
+table.calendar tbody th.out-month
+{
+    background-color: #ffffff;
+
+    color: #888888;
+}
+
+/* Each row of day-event-data for a week is fixed height */
+table.calendar tbody tr.week-data
+{
+    height: 1em;
+}
+
+/* The days are separated by borders */
+table.calendar tbody td
+{
+    border: 1px solid #d8d8d8;
+
+    border-style: none solid;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svv/cal.py	Sat Jan 08 22:52:25 2011 +0200
@@ -0,0 +1,222 @@
+"""
+    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 = 6
+    
+    # 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 :
+            yield tags.tr(class_='week-data')(
+                (
+                    tags.td(
+                        tags.a(href=self.url_for(urls.OrderView, id=order.id))(order.event_name)
+
+                    ) if order.on_date(date) else (
+                        tags.td("")
+
+                    )
+                ) for date in week
+            )
+
+    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),
+        )
+
--- a/svv/controllers.py	Fri Jan 07 03:45:19 2011 +0200
+++ b/svv/controllers.py	Sat Jan 08 22:52:25 2011 +0200
@@ -104,6 +104,7 @@
                 "/static/style.css", 
                 "/static/forms.css",
                 "/static/tables.css",
+                "/static/cal.css",
 
                 "/static/treelist.css",
 
--- a/svv/orders.py	Fri Jan 07 03:45:19 2011 +0200
+++ b/svv/orders.py	Sat Jan 08 22:52:25 2011 +0200
@@ -94,6 +94,13 @@
         return "%s %s" % (date, time)
 
 
+    def on_date (self, date) :
+        """
+            Does the event take place on this date?
+        """
+
+        return self.event_start.date() <= date <= self.event_end.date()
+
 # bind against database schema
 db.mapper(Customer, db.customers)
 db.mapper(Contact, db.contacts)
--- a/svv/urls.py	Fri Jan 07 03:45:19 2011 +0200
+++ b/svv/urls.py	Sat Jan 08 22:52:25 2011 +0200
@@ -8,6 +8,7 @@
 from svv.controllers import Index 
 from svv.customers import CustomersView, CustomerView
 from svv.orders import OrdersView, OrderView, EditOrderView, NewOrderView, OrderContractDocument
+from svv.cal import CalendarView
 
 # map URLs -> AppHandler
 URLS = Map((
@@ -16,6 +17,8 @@
     Rule('/orders/<int:id>', endpoint=OrderView),
     Rule('/orders/<int:id>/edit', endpoint=EditOrderView),
     Rule('/orders/<int:id>/Vuokrasopimus.pdf', endpoint=OrderContractDocument),
+    Rule('/calendar/', endpoint=CalendarView),
+    Rule('/calendar/<string:yearmonth>', endpoint=CalendarView),
 
     Rule('/customers', endpoint=CustomersView),
     Rule('/customers/<int:id>', endpoint=CustomerView),