changeset 452:333232953771

Initial check-in of sawmill a simple mod_python based web application to render build reports of treepkg.
author Sascha Teichmann <teichmann@intevation.de>
date Fri, 20 Aug 2010 16:15:29 +0000
parents eacfd3744d16
children dde2a0b68dc7
files contrib/sawmill/README contrib/sawmill/web/details.py contrib/sawmill/web/img/bottomleft-inner.png contrib/sawmill/web/img/bottomleft.png contrib/sawmill/web/img/bottomright-inner.png contrib/sawmill/web/img/bottomright.png contrib/sawmill/web/img/clear.png contrib/sawmill/web/img/logo.jpg contrib/sawmill/web/img/topleft-inner.png contrib/sawmill/web/img/topleft.png contrib/sawmill/web/img/topright-inner.png contrib/sawmill/web/img/topright.png contrib/sawmill/web/index.py contrib/sawmill/web/styles/style.css contrib/sawmill/web/templates/details.html contrib/sawmill/web/templates/overview.html contrib/sawmill/web/treepkgs/demo/treepkg.xml
diffstat 17 files changed, 602 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/sawmill/README	Fri Aug 20 16:15:29 2010 +0000
@@ -0,0 +1,2 @@
+A simple mod_python based web application to render the 
+build reports of treepkg.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/sawmill/web/details.py	Fri Aug 20 16:15:29 2010 +0000
@@ -0,0 +1,155 @@
+# -*- coding: UTF-8 -*-
+#
+# Copyright (C) 2010 by Intevation GmbH
+# Authors:
+# Sascha L. Teichmann <sascha.teichmann@intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+from mod_python import apache, psp, util
+
+import os
+import re
+import datetime
+import time
+
+from lxml import etree
+
+BASE_DIR = "treepkgs"
+
+TREEPKG_DIR = os.path.join(os.path.dirname(__file__), BASE_DIR)
+
+STATUS_LINE = re.compile(r"^([^:]+):(.+)")
+
+UNDER_SCORE = re.compile(r"_+(\w)")
+
+def _create_time(s, format="%Y-%m-%d %H:%M:%S"):
+    return datetime.datetime(*(time.strptime(s, format)[0:6]))
+
+def _pretty_log_name(log):
+    log = log.replace(".txt", "").replace(".gz", "").capitalize()
+    return UNDER_SCORE.sub(lambda x: " %s" % x.group(1).upper(), log)
+
+class TrackItem(object):
+
+    def __init__(self, treepkg, track, revision, status_file):
+        self.treepkg     = treepkg
+        self.track       = track
+        self.revision    = revision
+        self.status_file = status_file
+        self.loaded      = False
+        self.status      = None
+        self.start       = None
+        self.stop        = None
+        self.logs        = None
+
+    def check_loaded(self):
+        if not self.loaded:
+            f = open(self.status_file)
+            try:
+                for line in f:
+                    m = STATUS_LINE.match(line)
+                    if not m: continue
+                    key, value = [x.strip() for x in m.groups()]
+
+                    if   key == 'status': self.status = value
+                    elif key == 'start':  self.start  = _create_time(value)
+                    elif key == 'stop':   self.stop   = _create_time(value)
+            finally:
+                f.close()
+            self.loaded = True
+
+    def get_build_status(self):
+        self.check_loaded()
+        return self.status
+
+    def get_build_start(self):
+        self.check_loaded()
+        return self.start
+
+    def get_build_stop(self):
+        self.check_loaded()
+        return self.stop
+
+    def log_path(self, log):
+        return "%s/tracks/%s/pkg/%s/log/%s" % (
+            self.treepkg, self.track, self.revision, log)
+
+    def get_build_logs(self):
+        oj = os.path.join
+        if self.logs is None:
+            log_dir = oj(os.path.dirname(self.status_file), "log")
+            if not os.path.isdir(log_dir):
+                self.logs = []
+            else:
+                self.logs =[(_pretty_log_name(f), self.log_path(f)) 
+                    for f in os.listdir(log_dir)
+                    if os.path.isfile(oj(log_dir, f)) and f.find("txt") >= 0]
+        return self.logs
+
+    build_status = property(get_build_status)
+    build_start  = property(get_build_start)
+    build_stop   = property(get_build_stop)
+    build_logs   = property(get_build_logs)
+
+
+def __scan_track_items(treepkg, path):
+    items = []
+
+    tracks_path = os.path.join(path, "tracks")
+    for track in os.listdir(tracks_path):
+        track_path = os.path.join(tracks_path, track)
+        if not os.path.isdir(track_path): continue
+        revisions_path = os.path.join(track_path, "pkg")
+        for revision in os.listdir(revisions_path):
+            revision_path = os.path.join(revisions_path, revision)
+            if not os.path.isdir(revision_path): continue
+            status_file = os.path.join(revision_path, "status")
+            if not os.path.isfile(status_file): continue
+            items.append(TrackItem(treepkg, track, revision, status_file))
+            
+    return items
+
+def __description_header(treepkg):
+    treepkg_xml = os.path.join(treepkg, "treepkg.xml")
+    if os.path.isfile(treepkg_xml):
+        xml = None
+        try:
+            xml = open(treepkg_xml, "rb")
+            dom = etree.parse(xml)
+        finally:
+            if xml: xml.close()
+
+        description = ''.join(dom.xpath("//description/text()"))
+        header = ''.join([etree.tostring(x, encoding="UTF-8", method="html")
+            for x in dom.xpath("//header/*")])
+        return description, header
+    return "unknown", ""
+
+def index(req, treepkg=''):
+    if not treepkg: util.redirect(req, "index.py")
+
+    found = None
+    for d in os.listdir(TREEPKG_DIR):
+        dp = os.path.join(TREEPKG_DIR, d)
+        if os.path.isdir(dp) and d == treepkg:
+            found = dp
+            break
+
+    if not found:
+        req.status = apache.HTTP_NOT_FOUND
+        return "requested TreePkg not found"
+
+    description, header = __description_header(found)
+
+    track_items = __scan_track_items(treepkg, found)
+    
+    req.content_type = 'text/html;charset=utf-8'    
+    template = psp.PSP(req, filename='templates/details.html')
+    template.run({
+        'base_dir':    BASE_DIR,
+        'description': description,
+        'header':      header,
+        'track_items': track_items
+    })
Binary file contrib/sawmill/web/img/bottomleft-inner.png has changed
Binary file contrib/sawmill/web/img/bottomleft.png has changed
Binary file contrib/sawmill/web/img/bottomright-inner.png has changed
Binary file contrib/sawmill/web/img/bottomright.png has changed
Binary file contrib/sawmill/web/img/clear.png has changed
Binary file contrib/sawmill/web/img/logo.jpg has changed
Binary file contrib/sawmill/web/img/topleft-inner.png has changed
Binary file contrib/sawmill/web/img/topleft.png has changed
Binary file contrib/sawmill/web/img/topright-inner.png has changed
Binary file contrib/sawmill/web/img/topright.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/sawmill/web/index.py	Fri Aug 20 16:15:29 2010 +0000
@@ -0,0 +1,40 @@
+# -*- coding: UTF-8 -*-
+#
+# Copyright (C) 2010 by Intevation GmbH
+# Authors:
+# Sascha L. Teichmann <sascha.teichmann@intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+from mod_python import psp
+
+import os
+
+from lxml import etree
+
+TREEPKG_DIR = os.path.join(os.path.dirname(__file__), "treepkgs")
+
+def index(req):
+    req.content_type = 'text/html;charset=utf-8'    
+    template = psp.PSP(req, filename='templates/overview.html')
+
+    descriptions = []
+
+    for f in os.listdir(TREEPKG_DIR):
+        d = os.path.join(TREEPKG_DIR, f)
+        if not os.path.isdir(d): continue
+        treepkg_xml = os.path.join(d, "treepkg.xml")
+        if not os.path.isfile(treepkg_xml): continue
+        xml = None
+        try:
+            xml = open(treepkg_xml, "rb")
+            dom = etree.parse(xml)
+        finally:
+            if xml: xml.close()
+
+        description = ''.join(dom.xpath("//description/text()"))
+
+        descriptions.append((os.path.basename(d), description))
+
+    template.run({'descriptions': descriptions})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/sawmill/web/styles/style.css	Fri Aug 20 16:15:29 2010 +0000
@@ -0,0 +1,54 @@
+body {
+  margin-top: 3;
+  margin-left: 3;
+  margin-right: 3;
+  margin-bottom: 3;
+  background-color:#6190c0;
+}
+
+ol,ul,p,body,td,tr,th,form { 
+  font-family: verdana,arial,helvetica,sans-serif;
+  font-size:small;
+  color: #333333;
+}
+
+h1 { font-size: x-large; font-family: verdana,arial,helvetica,sans-serif; }
+h2 { font-size: large; font-family: verdana,arial,helvetica,sans-serif; }
+h3 { font-size: medium; font-family: verdana,arial,helvetica,sans-serif; }
+h4 { font-size: small; font-family: verdana,arial,helvetica,sans-serif; }
+h5 { font-size: x-small; font-family: verdana,arial,helvetica,sans-serif; }
+h6 { font-size: xx-small; font-family: verdana,arial,helvetica,sans-serif; }
+
+pre,tt { font-family: courier,sans-serif }
+
+a:link { text-decoration:none; color: #0000be }
+a:visited { text-decoration:none; color: #0000be }
+a:active { text-decoration:none }
+a:hover { text-decoration:underline; color:red }
+
+.titlebar { color: black; text-decoration: none; font-weight: bold; }
+a.tablink { color: black; text-decoration: none; font-weight: bold; font-size: x-small; }
+a.tablink:visited { color: black; text-decoration: none; font-weight: bold; font-size: x-small; }
+a.tablink:hover { text-decoration: none; color: black; font-weight: bold; font-size: x-small; }
+a.tabsellink { color: #0000be; text-decoration: none; font-weight: bold; font-size: x-small; }
+a.tabsellink:visited { color: #0000be; text-decoration: none; font-weight: bold; font-size: x-small; }
+a.tabsellink:hover { text-decoration: none; color: #0000be; font-weight: bold; font-size: x-small; }
+
+.css_prison {}
+
+.css_prison .statustable { background:#F4F4F4; width:95% }
+.css_prison .statustablestatus { background:#E0E0E0; width:15% }
+.css_prison .statustablepkg { background:#E0E0E0; width:45% }
+.css_prison .statustablenotes { background:#E0E0E0; width:10% }
+.css_prison .statustablehead { background:#E0E0E0; }
+.css_prison .statusheading { font-weight:bold; }
+.css_prison .finished { background:#C0FFC0; }
+.css_prison .inprogress { background:#FFFFC0; }
+.css_prison .error { background:#FFC0C0; }
+.css_prison .date_row {
+  background:#F0F0F0;
+  font-weight:bold; font-size:smaller;
+  text-align:center;
+}
+.css_prison tr { background:#FFFFFF; }
+.css_prison td { padding:5px; }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/sawmill/web/templates/details.html	Fri Aug 20 16:15:29 2010 +0000
@@ -0,0 +1,206 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
+  "http://www.w3.org/TR/html4/loose.dtd">
+<%
+from cgi import escape
+from xml.sax.saxutils import quoteattr
+
+from datetime import date
+%>
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+        <link rel="stylesheet" href="styles/style.css" type="text/css" media="screen" />
+        <title>S&auml;gewerker - Free Software forestry</title>
+ </head>
+
+ <body>
+  <table border="0" width="100%" cellspacing="0" cellpadding="0">
+   <tr>
+    <td>
+     <a href="/"><img src="img/logo.jpg" border="0" alt="" width="533" height="94" /></a>
+    </td>
+   </tr>
+  </table>
+  <table border="0" width="100%" cellspacing="0" cellpadding="0">
+
+   <tr>
+        <td align="left" bgcolor="#E0E0E0" width="9">
+     <img src="img/topleft.png" height="9" width="9" alt="" />
+    </td>
+        <td bgcolor="#E0E0E0" width="30">
+     <img src="img/clear.png" width="30" height="1" alt="" />
+    </td>
+        <td bgcolor="#E0E0E0">
+     <img src="img/clear.png" width="1" height="1" alt="" />
+
+    </td>
+        <td bgcolor="#E0E0E0" width="30">
+     <img src="img/clear.png" width="30" height="1" alt="" />
+    </td>
+        <td align="right" bgcolor="#E0E0E0" width="9">
+     <img src="img/topright.png" height="9" width="9" alt="" />
+    </td>
+     </tr>
+
+     <tr>
+    <!-- Outer body row -->
+    <td bgcolor="#E0E0E0">
+     <img src="img/clear.png" width="10" height="1" alt="" />
+    </td>
+        <td valign="top" width="99%" bgcolor="#E0E0E0" colspan="3">
+       <!-- Inner Tabs / Shell -->
+         <table border="0" width="100%" cellspacing="0" cellpadding="0">
+          <tr>
+
+             <td align="left" bgcolor="#ffffff" width="9">
+        <img src="img/topleft-inner.png" height="9" width="9" alt="" />
+       </td>
+             <td bgcolor="#ffffff">
+        <img src="img/clear.png" width="1" height="1" alt="" />
+       </td>
+             <td align="right" bgcolor="#ffffff" width="9">
+        <img src="img/topright-inner.png" height="9" width="9" alt="" />
+       </td>
+
+            </tr>
+            <tr>
+             <td bgcolor="#ffffff">
+        <img src="img/clear.png" width="10" height="1" alt="" />
+       </td>
+             <td valign="top" width="99%" bgcolor="white" class="css_prison">
+        <!-- end main body row -->
+<h1><%= escape(description) %></h1>
+<%= header %>
+<table class="statustable">
+<tr>
+    <th class="statustablehead">Status</th>
+    <th class="statustablehead">Package</th>
+    <th class="statustablehead">Revision</th>
+    <th class="statustablehead">Start</th>
+    <th class="statustablehead">Stop</th>
+    <th class="statustablehead">Notes</th>
+</tr>
+<%
+
+def nn(s, d=""):
+    if not s: return d
+    return escape(s)
+
+def pretty_time(t, format="%Y-%m-%d %H:%M:%S"):
+    if not t: return "&lt;unknown&gt;"
+    return t.strftime(format)
+
+
+def date_from_datetime(x):
+    if not x: return None
+    return date(x.year, x.month, x.day)
+
+def sort_by_start(a, b):
+    a_start = a.build_start
+    b_start = b.build_start
+    if not a_start and not b_start: return 0
+    if not a_start: return 1
+    if not b_start: return -1
+    return cmp(a_start, b_start)
+
+track_items = sorted(track_items, cmp=sort_by_start, reverse=True)
+
+last_date = None
+
+STATUS2CLASS = {
+    'creating_binary_package': 'inprogress',
+    'binary_package_created':  'finished'
+}
+
+STATUS2MSG = {
+    'creating_binary_package': 'building binary packages',
+    'binary_package_created':  'build successful'
+}
+
+for track_item in track_items:
+    # for all track items
+    curr_date = date_from_datetime(track_item.build_start)
+    if curr_date != last_date:
+        last_date = curr_date
+%>
+<tr class="date_row"><td colspan="6"><%= pretty_time(last_date, "%Y-%m-%d") %></td></tr>
+<%
+    # date changed
+%>
+<tr class="<%= STATUS2CLASS.get(track_item.build_status, 'error') %>">
+    <td><%= STATUS2MSG.get(track_item.build_status, 'error') %></td>
+    <td style="font-weight:bold;"><%= nn(track_item.track) %></td>
+    <td align="right"><%= nn(track_item.revision) %></td>
+    <td><%= pretty_time(track_item.build_start) %></td>
+    <td><%= pretty_time(track_item.build_stop) %></td>
+    <td>
+<%
+    for log_desc, log_path in track_item.build_logs:
+        # for all logs
+%>
+[<a href=<%= quoteattr("%s/%s" % (base_dir, log_path)) %>><%= nn(log_desc) %></a>]
+
+<%
+    # for all logs
+%>
+    </td>
+</tr>
+
+<%
+# for all track itemes
+%>
+
+</table>
+
+
+      <!-- end main body row -->
+             </td>
+             <td width="10" bgcolor="#ffffff">
+        <img src="img/clear.png" width="2" height="1" alt="" />
+       </td>
+            </tr>
+
+            <tr>
+             <td align="left" bgcolor="#E0E0E0" width="9">
+        <img src="img/bottomleft-inner.png" height="11" width="11" alt="" />
+        </td>
+                <td bgcolor="#ffffff">
+         <img src="img/clear.png" width="1" height="1" alt="" />
+        </td>
+                <td align="right" bgcolor="#E0E0E0" width="9">
+         <img src="img/bottomright-inner.png" height="11" width="11" alt="" />
+
+        </td>
+             </tr>
+            </table>
+
+          <!-- end inner body row -->
+
+         </td>
+         <td width="10" bgcolor="#E0E0E0">
+      <img src="img/clear.png" width="2" height="1" alt="" />
+     </td>
+
+      </tr>
+      <tr>
+         <td align="left" bgcolor="#E0E0E0" width="9">
+      <img src="img/bottomleft.png" height="9" width="9" alt="" />
+     </td>
+         <td bgcolor="#E0E0E0" colspan="3">
+      <img src="img/clear.png" width="1" height="1" alt="" />
+     </td>
+         <td align="right" bgcolor="#E0E0E0" width="9">
+
+      <img src="img/bottomright.png" height="9" width="9" alt="" />
+     </td>
+      </tr>
+   </table>
+   <br />
+   <center>
+    <b style="color:white; font-size:13px;"> 
+     This site is hosted by the <a href="http://www.intevation.de">Intevation GmbH</a>
+    </b>
+   </center>
+
+    </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/sawmill/web/templates/overview.html	Fri Aug 20 16:15:29 2010 +0000
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
+  "http://www.w3.org/TR/html4/loose.dtd">
+<%
+from cgi import escape
+from xml.sax.saxutils import quoteattr
+%>
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+        <link rel="stylesheet" href="styles/style.css" type="text/css" media="screen" />
+        <title>S&auml;gewerker - Free Software forestry</title>
+ </head>
+
+ <body>
+  <table border="0" width="100%" cellspacing="0" cellpadding="0">
+   <tr>
+    <td>
+     <a href="/"><img src="img/logo.jpg" border="0" alt="" width="533" height="94" /></a>
+    </td>
+   </tr>
+  </table>
+  <table border="0" width="100%" cellspacing="0" cellpadding="0">
+
+   <tr>
+        <td align="left" bgcolor="#E0E0E0" width="9">
+     <img src="img/topleft.png" height="9" width="9" alt="" />
+    </td>
+        <td bgcolor="#E0E0E0" width="30">
+     <img src="img/clear.png" width="30" height="1" alt="" />
+    </td>
+        <td bgcolor="#E0E0E0">
+     <img src="img/clear.png" width="1" height="1" alt="" />
+
+    </td>
+        <td bgcolor="#E0E0E0" width="30">
+     <img src="img/clear.png" width="30" height="1" alt="" />
+    </td>
+        <td align="right" bgcolor="#E0E0E0" width="9">
+     <img src="img/topright.png" height="9" width="9" alt="" />
+    </td>
+     </tr>
+
+     <tr>
+    <!-- Outer body row -->
+    <td bgcolor="#E0E0E0">
+     <img src="img/clear.png" width="10" height="1" alt="" />
+    </td>
+        <td valign="top" width="99%" bgcolor="#E0E0E0" colspan="3">
+       <!-- Inner Tabs / Shell -->
+         <table border="0" width="100%" cellspacing="0" cellpadding="0">
+          <tr>
+
+             <td align="left" bgcolor="#ffffff" width="9">
+        <img src="img/topleft-inner.png" height="9" width="9" alt="" />
+       </td>
+             <td bgcolor="#ffffff">
+        <img src="img/clear.png" width="1" height="1" alt="" />
+       </td>
+             <td align="right" bgcolor="#ffffff" width="9">
+        <img src="img/topright-inner.png" height="9" width="9" alt="" />
+       </td>
+
+            </tr>
+            <tr>
+             <td bgcolor="#ffffff">
+        <img src="img/clear.png" width="10" height="1" alt="" />
+       </td>
+             <td valign="top" width="99%" bgcolor="white">
+        <!-- end main body row -->
+         <div style="text-align: right"><a href="http://wald.intevation.de/">...to WALD source code repository</a></div>
+
+<% 
+for treepkg, description in descriptions: 
+    # for all descriptions
+%>
+<div style="magin:5px;border-bottom:1px solid #DDDDDD;padding:5px;font-weight:bold; font-size: 16pt">
+<a href=<%= quoteattr("details.py?treepkg=%s" % treepkg) %>><%= escape(description) %></a>
+</div>
+<%
+# for all descriptions
+%>
+              <!-- end main body row -->
+             </td>
+             <td width="10" bgcolor="#ffffff">
+        <img src="img/clear.png" width="2" height="1" alt="" />
+       </td>
+            </tr>
+
+            <tr>
+             <td align="left" bgcolor="#E0E0E0" width="9">
+        <img src="img/bottomleft-inner.png" height="11" width="11" alt="" />
+        </td>
+                <td bgcolor="#ffffff">
+         <img src="img/clear.png" width="1" height="1" alt="" />
+        </td>
+                <td align="right" bgcolor="#E0E0E0" width="9">
+         <img src="img/bottomright-inner.png" height="11" width="11" alt="" />
+
+        </td>
+             </tr>
+            </table>
+
+          <!-- end inner body row -->
+
+         </td>
+         <td width="10" bgcolor="#E0E0E0">
+      <img src="img/clear.png" width="2" height="1" alt="" />
+     </td>
+
+      </tr>
+      <tr>
+         <td align="left" bgcolor="#E0E0E0" width="9">
+      <img src="img/bottomleft.png" height="9" width="9" alt="" />
+     </td>
+         <td bgcolor="#E0E0E0" colspan="3">
+      <img src="img/clear.png" width="1" height="1" alt="" />
+     </td>
+         <td align="right" bgcolor="#E0E0E0" width="9">
+
+      <img src="img/bottomright.png" height="9" width="9" alt="" />
+     </td>
+      </tr>
+   </table>
+   <br />
+   <center>
+    <b style="color:white; font-size:13px;"> 
+     This site is hosted by the <a href="http://www.intevation.de">Intevation GmbH</a>
+
+    </b>
+   </center>
+
+    </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/sawmill/web/treepkgs/demo/treepkg.xml	Fri Aug 20 16:15:29 2010 +0000
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<treepkg>
+    <description>This is the status of the demo packager</description>
+    <header>
+        <div xmlns="http://www.w3.org/1999/xhtml">
+            <p>
+              Put extra project information here.
+              <strong>Any type of HTML tags are allowed here</strong>
+            </p>
+        </div>
+    </header>
+</treepkg>
This site is hosted by Intevation GmbH (Datenschutzerklärung und Impressum | Privacy Policy and Imprint)