Index: ticket/report.py
===================================================================
--- ticket/report.py
+++ ticket/report.py
@@ -28,11 +28,39 @@
 from trac.wiki import wiki_to_html, IWikiSyntaxProvider
 
 
+try:
+    from pyExcelerator import *
+    class XlsDoc(CompoundDoc.XlsDoc):
+        def get(self, stream):
+            padding = '\x00' * (0x1000 - (len(stream) % 0x1000))
+            self.book_stream_len = len(stream) + len(padding)
+            self.__build_directory()
+            self.__build_sat()
+            self.__build_header()
+            return '%s%s%s%s%s%s%s' % (
+                self.header,
+                self.packed_MSAT_1st,
+                stream,
+                padding,
+                self.packed_MSAT_2nd,
+                self.packed_SAT,
+                self.dir_stream)
+
+    class Workbook(Workbook):
+        def get(self):
+           doc = XlsDoc()
+           return doc.get(self.get_biff_data())
+
+    has_pyexcel = 1
+except:
+    has_pyexcel = 0
+
 class ColumnSorter:
 
-    def __init__(self, columnIndex, asc=1):
+    def __init__(self, columnIndex, asc=1, enums=None):
         self.columnIndex = columnIndex
         self.asc = asc
+        self.enums = enums
 
     def sort(self, x, y):
         const = -1
@@ -42,10 +70,16 @@
         # make sure to ignore case in comparisons
         realX = x[self.columnIndex]
         if isinstance(realX, (str, unicode)):
-            realX = realX.lower()
+            if self.enums and self.enums.has_key(realX):
+                realX = self.enums[realX]
+            else:
+                realX = realX.lower()
         realY = y[self.columnIndex]
         if isinstance(realY, (str, unicode)):
-            realY = realY.lower()
+            if self.enums and self.enums.has_key(realY):
+                realY = self.enums[realY]
+            else:
+                realY = realY.lower()
 
         result = 0
         if realX < realY:
@@ -305,11 +339,19 @@
             if colIndex != None:
                 k = 'report.headers.%d.asc' % (colIndex - hiddenCols)
                 asc = req.args.get('asc', None)
+                enums = None
+                from trac.ticket import model
+                clses = {'severity':model.Severity, 'priority':model.Priority,
+                         'resolution':model.Resolution,
+                         'type':model.Type, 'status':model.Status}
+                if clses.has_key(sortCol):
+                    scls = clses[sortCol]
+                    enums = dict([(enum.name, enum.value) for enum in scls.select(self.env, db=db)])
                 if asc:
-                    sorter = ColumnSorter(colIndex, int(asc))
+                    sorter = ColumnSorter(colIndex, int(asc), enums=enums)
                     req.hdf[k] = asc
                 else:
-                    sorter = ColumnSorter(colIndex)
+                    sorter = ColumnSorter(colIndex, enums=enums)
                     req.hdf[k] = 1
                 rows.sort(sorter.sort)
 
@@ -372,6 +414,9 @@
         elif format == 'tab':
             self._render_csv(req, cols, rows, '\t')
             return None
+        elif format == 'xls':
+            self._render_xls(req, cols, rows)
+            return None
 
         return 'report.cs', None
 
@@ -390,6 +435,10 @@
                  'Comma-delimited Text', 'text/plain')
         add_link(req, 'alternate', '?format=tab' + href,
                  'Tab-delimited Text', 'text/plain')
+        if has_pyexcel:
+            if self.env.config.getbool('ticket', 'show_excel_link'):
+                add_link(req, 'alternate', '?format=xls' + href,
+                         'Excel', 'application/vnd.ms-excel')
         if req.perm.has_permission('REPORT_SQL_VIEW'):
             add_link(req, 'alternate', '?format=sql', 'SQL Query',
                      'text/plain')
@@ -485,6 +534,82 @@
             req.write('-- %s\n\n' % '\n-- '.join(description.splitlines()))
         req.write(sql)
         
+    def _render_xls(self, req, cols, rows):
+        req.send_response(200)
+        req.send_header('Content-Type', 'application/vnd.ms-excel')
+        req.send_header('Content-Disposition',
+                        'filename=Report%s.xls' % req.hdf['report.id'])
+        req.end_headers()
+
+        wb = Workbook()
+        sheetname = "%s - %s" % (req.hdf['report.title'],
+                                 req.hdf['project.name'])
+        ws = wb.add_sheet(sheetname.decode('utf-8'))
+        ws.panes_frozen = True
+        ro = 1
+        ws.horz_split_pos = ro
+
+        import copy
+
+        font0 = Font()
+        font0.charset = font0.CHARSET_SYS_DEFAULT
+        font0.name = 'MS UI Gothic'
+        font1 = copy.copy(font0)
+        font1.bold = True
+        font2 = copy.copy(font0)
+        font2.height = 0x00A0
+        align0 = Alignment()
+        align1 = copy.copy(align0)
+        align1.vert = align1.VERT_TOP
+        align2 = copy.copy(align1)
+        align2.wrap = align2.WRAP_AT_RIGHT
+
+        style0 = XFStyle()
+        style0.font = font0
+        style0.alignment = align1
+        style0.num_format_str = 'general'
+        style_colheader = copy.copy(style0)
+        style_colheader.num_format_str = '@'
+        style_colheader.font = font1
+        style_num = copy.copy(style0)
+        style_str = copy.copy(style0)
+        style_str.num_format_str = '@'
+        style_wrap_str = copy.copy(style0)
+        style_wrap_str.alignment = align2
+        style_wrap_str.font = font2
+        style_date = copy.copy(style0)
+        style_date.num_format_str = 'yyyy/mm/dd'
+
+        for col, cx in map(lambda x, y: [x, y], cols, range(len(cols))):
+            name = str(col[0]).replace('_','')
+            ws.write(ro-1, cx, name.decode('utf-8'), style_colheader)
+            conv = lambda x: str(x).replace('\r', '') \
+                                   .rstrip('\r\n') \
+                                   .decode('utf-8')
+            style = style_str
+            if name in ['time', 'date','changetime', 'created', 'modified']:
+                ws.col(cx).width = 0xb00
+                from datetime import datetime
+                conv = lambda x: datetime.fromtimestamp(float(x))
+                style = style_date
+            elif name in ['summary', 'description']:
+                if name == 'description':
+                    ws.col(cx).width = 0x7000
+                else:
+                    ws.col(cx).width = 0x1a00
+                style = style_wrap_str
+            elif name in ['color', 'ticket', 'id']:
+                if name in ['color']:
+                    ws.col(cx).hidden = 1
+                conv = lambda x: int(x)
+                style = style_num
+            elif name in ['style']:
+                ws.col(cx).hidden = 1
+            for value, rx in map(lambda x, y: [conv(x[cx]), ro + y], \
+                                 rows, range(len(rows))):
+                ws.write(rx, cx, value, style)
+        req.write(wb.get())
+
     # IWikiSyntaxProvider methods
     
     def get_link_resolvers(self):

