implement browse-by-date to show a nice calendar
authorTero Marttila <terom@fixme.fi>
Mon, 09 Feb 2009 04:39:24 +0200
changeset 54 b65a95eb9f6b
parent 53 8103d18907a0
child 55 5667d2bbdc50
implement browse-by-date to show a nice calendar
handlers.py
helpers.py
log_source.py
static/irclogs.css
templates/channel.tmpl
templates/channel_calendar.tmpl
urls.py
--- a/handlers.py	Mon Feb 09 03:05:43 2009 +0200
+++ b/handlers.py	Mon Feb 09 04:39:24 2009 +0200
@@ -2,7 +2,7 @@
     Our URL action handlers
 """
 
-import pytz
+import datetime, calendar, pytz
 
 from qmsk.web import http, template
 
@@ -12,7 +12,7 @@
 
 # load templates from here
 templates = template.TemplateLoader("templates",
-    h               = helpers,
+    _helper_class   = helpers.Helpers,
     urls            = urls,
     channel_list    = channels.channel_list,
 )
@@ -33,8 +33,8 @@
    
     return http.Redirect(urls.channel_view.build(request, channel=channel.id))
 
-@preferences.handler(prefs.Formatter)
-def channel_view (request, channel, count, formatter) :
+@preferences.handler(prefs.Formatter, prefs.Timezone)
+def channel_view (request, channel, count, formatter, timezone) :
     """
         The main channel view page, display the most important info, and all requisite links
     """
@@ -47,6 +47,7 @@
 
     return templates.render_to_response("channel_view",
         req             = request,
+        timezone        = timezone,
         channel         = channel,
         count           = count,
         formatter       = formatter,
@@ -66,12 +67,34 @@
     else :
         raise http.ResponseError("Unknown filetype %r" % format)
 
-def channel_calendar (request, channel) :
+@preferences.handler(prefs.Timezone)
+def channel_calendar (request, channel, year, month, timezone) :
     """
-        Display a list of avilable logs for some days
+        Display a list of avilable logs for some month
     """
 
-    pass
+    # current date as default
+    now = timezone.localize(datetime.datetime.now())
+
+    # target year/month
+    target = timezone.localize(datetime.datetime(
+        year    = year if year else now.year,
+        month   = month if month else now.month,
+        day     = 1
+    ))
+
+    # get set of days available
+    days = channel.source.get_month_days(target)
+
+    # display calendar
+    return templates.render_to_response("channel_calendar",
+        req             = request,
+        timezone        = timezone,
+        channel         = channel,
+        calendar        = calendar.Calendar(),
+        month           = target.date(),
+        days            = days,
+    )
 
 @preferences.handler(prefs.Formatter, prefs.Timezone)
 def channel_date (request, channel, date, formatter, timezone) :
@@ -90,6 +113,7 @@
 
     return templates.render_to_response("channel_date",
         req             = request,
+        timezone        = timezone,
         channel         = channel,
         formatter       = formatter,
         date            = date,
--- a/helpers.py	Mon Feb 09 03:05:43 2009 +0200
+++ b/helpers.py	Mon Feb 09 04:39:24 2009 +0200
@@ -2,21 +2,56 @@
     Some additional helpers
 """
 
-# "inherit" qmsk.web's helpers
-from qmsk.web.helpers import *
+import qmsk.web.helpers
 
-def tz_name (tz) :
+import datetime, calendar
+
+class Helpers (qmsk.web.helpers.Helpers) :
     """
-        Returns a string describing the given timezone
+        Our set of helpers, inheriting from base helpers
     """
 
-    return str(tz)
+    def tz_name (self, tz) :
+        """
+            Returns a string describing the given timezone
+        """
 
-def fmt_date (date) :
-    """
-        Formats a date
-    """
-    
-    # XXX: hardcoded
-    return date.strftime('%Y-%m-%d')
+        return str(tz)
 
+    def fmt_date (self, date) :
+        """
+            Formats a date
+        """
+        
+        # XXX: hardcoded
+        return date.strftime('%Y-%m-%d')
+
+    def fmt_month (self, date) :
+        """
+            Formats a month
+        """
+
+        return date.strftime('%B %Y')
+        
+    def fmt_weekday (self, wday) :
+        """
+            Formats an abbreviated weekday name
+        """
+
+        return calendar.day_abbr[wday]
+
+    def build_date (self, month, mday) :
+        """
+            Returns a datetime.date for the given (month.year, month.month, mday)
+        """
+
+        return datetime.date(month.year, month.month, mday)
+
+    def is_today (self, date) :
+        """
+            checks if the given date is today
+        """
+
+        # construct current date
+        return date == self.ctx['timezone'].localize(datetime.datetime.now()).date()
+        
--- a/log_source.py	Mon Feb 09 03:05:43 2009 +0200
+++ b/log_source.py	Mon Feb 09 04:39:24 2009 +0200
@@ -2,7 +2,7 @@
     A source of IRC log files
 """
 
-import datetime, itertools
+import datetime, calendar, itertools
 import os, errno
 import pytz
 
@@ -24,10 +24,19 @@
         """
 
         abstract
+    
+    def get_month_days (self, dt) :
+        """
+            Get a set of dates, telling which days in the given month (as a datetime) have logs available
+        """
 
-class LogFile (LogSource) :
+        abstract
+        
+class LogFile (object) :
     """
         A file containing LogEvents
+
+        XXX: modify to implement LogSource?
     """
 
     def __init__ (self, path, parser, start_date=None, charset='utf-8', sep='\n') :
@@ -168,7 +177,7 @@
             for line in lines[:0:-1] :
                 yield line.decode(self.charset)
 
-    def get_latest (self, count) :
+    def read_latest (self, count) :
         """
             Returns up to count events, from the end of the file, or less, if the file doesn't contain that many lines.
         """
@@ -221,9 +230,12 @@
         # convert to date and use that
         return self._get_logfile_date(dtz.date())
 
-    def _get_logfile_date (self, d) :
+    def _get_logfile_date (self, d, load=True) :
         """
-            Get the logfile corresponding to the given naive date in our timezone
+            Get the logfile corresponding to the given naive date in our timezone. If load is False, only test for the
+            presence of the logfile, do not actually open it.
+
+            Returns None if the logfile does not exist.
         """
 
         # format filename
@@ -231,9 +243,24 @@
 
         # build path
         path = os.path.join(self.path, filename)
+        
+        try :
+            if load :
+                # open+return the LogFile
+                return LogFile(path, self.parser, d, self.charset)
+            
+            else :
+                # test
+                return os.path.exists(path)
 
-        # return the LogFile
-        return LogFile(path, self.parser, d, self.charset)
+        # XXX: move to LogFile
+        except IOError, e :
+            # return None for missing files
+            if e.errno == errno.ENOENT :
+                return None
+
+            else :
+                raise
     
     def _iter_date_reverse (self, dt=None) :
         """
@@ -280,18 +307,13 @@
         while len(lines) < count :
             logfile = None
 
-            try :
-                # get next logfile
-                files += 1
-                
-                # open
-                logfile = self._get_logfile_date(day_iter.next())
+            # get next logfile
+            files += 1
             
-            except IOError, e :
-                # skip nonexistant days if we haven't found any logs yet
-                if e.errno != errno.ENOENT :
-                    raise
-
+            # open
+            logfile = self._get_logfile_date(day_iter.next())
+            
+            if not logfile :
                 if files > MAX_FILES :
                     raise Exception("No recent logfiles found")
                 
@@ -301,7 +323,7 @@
             
             # read the events
             # XXX: use a queue
-            lines = list(logfile.get_latest(count)) + lines
+            lines = list(logfile.read_latest(count)) + lines
         
         # return the events
         return lines
@@ -333,3 +355,27 @@
             # chain together the two sources
             return itertools.chain(f_begin.read_from(dtz_begin), f_end.read_until(dtz_end))
 
+    def get_month_days (self, month) :
+        """
+            Returns a set of dates for which logfiles are available in the given datetime's month
+        """
+        
+        # the set of days
+        days = set()
+        
+        # iterate over month's days using Calendar
+        for date in calendar.Calendar().itermonthdates(month.year, month.month) :
+            # convert date to target datetime
+            dtz = month.tzinfo.localize(datetime.datetime.combine(date, datetime.time(0))).astimezone(self.tz)
+
+            # date in our target timezone
+            log_date = dtz.date()
+            
+            # test for it
+            if self._get_logfile_date(log_date, load=False) :
+                # add to set
+                days.add(date)
+
+        # return set
+        return days
+
--- a/static/irclogs.css	Mon Feb 09 03:05:43 2009 +0200
+++ b/static/irclogs.css	Mon Feb 09 04:39:24 2009 +0200
@@ -125,4 +125,12 @@
     text-align: center;
 }
 
+/*
+ * General
+ */
 
+/* Calendar */
+table.calendar td#today {
+    font-weight: bold;
+}
+
--- a/templates/channel.tmpl	Mon Feb 09 03:05:43 2009 +0200
+++ b/templates/channel.tmpl	Mon Feb 09 04:39:24 2009 +0200
@@ -48,5 +48,5 @@
 ${next.body()}
 
 <%def name="footer_right()">
-    All times are in <strong>${h.tz_name(formatter.tz)}</strong>
+    All times are in <strong>${h.tz_name(timezone)}</strong>
 </%def>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/channel_calendar.tmpl	Mon Feb 09 04:39:24 2009 +0200
@@ -0,0 +1,47 @@
+<%inherit file="channel.tmpl" />
+
+<%def name="month_table(cal, month, dates)">
+<table class="calendar">
+## table header - month name
+    <tr>
+        <th colspan="7">${h.fmt_month(month)}</th>
+    </tr>
+## month header - weekday names    
+    <tr>
+    % for weekday in cal.iterweekdays() :
+        <th>${h.fmt_weekday(weekday)}</th>
+    % endfor
+    </tr>
+## iterate over the weeks
+% for week in cal.monthdays2calendar(month.year, month.month) :
+    <tr>
+    ## iterate over the week's days
+    % for day, weekday in week :
+        ## is it an empty cell?
+        % if not day :
+        <td>&nbsp;</td>
+        % else :
+        ## build date
+        <% date = h.build_date(month, day) %>
+        ## is it today?
+        % if day and h.is_today(date) :
+        <td id="today">\
+        % else :
+        <td>\
+        % endif
+        ## link to logs for this day?
+        % if date in dates :
+        <a href="${urls.channel_date.build(req, channel=channel, date=date)}">${day}</a>\
+        % else :
+        ${day}\
+        % endif
+</td>
+    % endif
+    % endfor
+    </tr>
+% endfor
+</table>
+</%def>
+
+${month_table(calendar, month, days)}
+
--- a/urls.py	Mon Feb 09 03:05:43 2009 +0200
+++ b/urls.py	Mon Feb 09 04:39:24 2009 +0200
@@ -27,13 +27,13 @@
 )
 
 # urls
-index           = url('/',                                                              handlers.index                  )
-channel_select  = url('/channel_select/?channel:cid',                                   handlers.channel_select         )
-channel_view    = url('/channels/{channel:cid}/?count:int=10',                          handlers.channel_view           )
-channel_last    = url('/channels/{channel:cid}/last/{count:int=100}/{format=html}',     handlers.channel_last           )
-channel_date    = url('/channels/{channel:cid}/calendar',                               handlers.channel_calendar       )
-channel_date    = url('/channels/{channel:cid}/date/{date:date}',                       handlers.channel_date           )
-channel_search  = url('/channels/{channel:cid}/search/?q',                              handlers.channel_search         )
+index               = url('/',                                                              handlers.index                  )
+channel_select      = url('/channel_select/?channel:cid',                                   handlers.channel_select         )
+channel_view        = url('/channels/{channel:cid}/?count:int=10',                          handlers.channel_view           )
+channel_last        = url('/channels/{channel:cid}/last/{count:int=100}/{format=html}',     handlers.channel_last           )
+channel_calendar    = url('/channels/{channel:cid}/calendar/{year:int=0}/{month:int=0}',    handlers.channel_calendar       )
+channel_date        = url('/channels/{channel:cid}/date/{date:date}',                       handlers.channel_date           )
+channel_search      = url('/channels/{channel:cid}/search/?q',                              handlers.channel_search         )
 
 # mapper
 mapper = urltree.URLTree(urls)