Major code restructuring. Version is now 0.5, templates use Mako, and the code is split off into several files under lib/
authorterom
Fri, 21 Dec 2007 20:36:03 +0000
changeset 12 c2d8e9a754a1
parent 11 27dac27d1a58
child 13 c229bcb1de41
Major code restructuring. Version is now 0.5, templates use Mako, and the code is split off into several files under lib/
degal.py
lib/__init__.py
lib/__init__.py
lib/folder.py
lib/folder.py
lib/formatbytes.py
lib/formatbytes.py
lib/helpers.py
lib/helpers.py
lib/image.py
lib/image.py
lib/log.py
lib/log.py
lib/settings.py
lib/settings.py
lib/shorturl.py
lib/shorturl.py
lib/template.py
lib/template.py
lib/utils.py
lib/utils.py
series.cgi
shorturl.cgi
taggr.cgi
templates/gallery.html
templates/image.html
templates/master.html
--- a/degal.py	Thu Dec 20 17:42:04 2007 +0000
+++ b/degal.py	Fri Dec 21 20:36:03 2007 +0000
@@ -21,647 +21,11 @@
 #
 
 import os.path, os
-import urllib
-import logging
-import string
-from datetime import datetime
-import struct
-import base64
-import shelve
-
-import PIL
-import PIL.Image
-
-import utils
-
-__version__ = '0.2'
-
-TEMPLATE_DIR='templates'
-TEMPLATE_EXT='html'
-
-logging.basicConfig(
-    level=logging.INFO,
-    format="%(message)s",
-#    format="%(name)8s %(levelname)8s   %(lineno)3d %(message)s",
-
-)
-
-tpl = logging.getLogger('tpl')
-index = logging.getLogger('index')
-render = logging.getLogger('render')
-
-tpl.setLevel(logging.WARNING)
-
-#for l in (tpl, index, prepare, render) :
-#    l.setLevel(logging.DEBUG)
-
-def readFile (path) :
-    fo = open(path, 'r')
-    data = fo.read()
-    fo.close()
-
-    return data
-
-class Template (object) :
-    GLOBALS = dict(
-        VERSION=__version__,
-    )
-
-    def __init__ (self, name) :
-        self.name = name
-        self.path = os.path.join(TEMPLATE_DIR, "%s.%s" % (name, TEMPLATE_EXT))
-
-        tpl.debug("Template %s at %s", name, self.path)
-
-        self.content = readFile(self.path)
-
-    def render (self, **vars) :
-        content = self.content
-        
-        vars.update(self.GLOBALS)
-
-        for name, value in vars.iteritems() :
-            content = content.replace('<!-- %s -->' % name, str(value))
-
-        return content
-
-    def renderTo (self, path, **vars) :
-        tpl.info("Render %s to %s", self.name, path)
-
-        fo = open(path, 'w')
-        fo.write(self.render(**vars))
-        fo.close()
-
-gallery_tpl = Template('gallery')
-image_tpl = Template('image')
-
-IMAGE_EXTS = ('jpg', 'jpeg', 'png', 'gif', 'bmp')
-
-THUMB_DIR = 'thumbs'
-PREVIEW_DIR = 'previews'
-TITLE_FILE = 'title.txt'
-
-THUMB_GEOM = (160, 120)
-PREVIEW_GEOM = (640, 480)
-
-DEFAULT_TITLE = 'Image gallery'
-
-# how many image/page
-IMAGE_COUNT = 50
-
-def isImage (fname) :
-    """
-        Is the given filename likely to be an image file?
-    """
-
-    fname = fname.lower()
-    base, ext = os.path.splitext(fname)
-    ext = ext.lstrip('.')
-
-    return ext in IMAGE_EXTS
-
-def link (url, title) :
-    """
-        Returns an <a href=""></a> link tag with the given values
-    """
-
-    return "<a href='%s'>%s</a>" % (urllib.quote(url), title)
-
-def dirUp (count=1) :
-    """
-        Returns a relative path to the directly count levels above the current one
-    """
-
-    if not count :
-        return '.'
-
-    return os.path.join(*(['..']*count))
-
-def readTitleDescr (path) :
-    """
-        Read a title.txt or <imgname>.txt file
-    """
-
-    if os.path.exists(path) :
-        content = readFile(path)
-
-        if '---' in content :
-            title, descr = content.split('---', 1)
-        else :
-            title, descr = content, ''
-
-        return title.strip(), descr.strip()
-
-    return None, None
-
-class Folder (object) :
-    def __init__ (self, name='.', parent=None) :
-        # the directory name
-        self.name = name
-
-        # our parent Folder, or None
-        self.parent = parent
-
-        # the path to this dir, as a relative path to the root of the image gallery, always starts with .
-        if parent and name :
-            self.path = parent.pathFor(name)
-        else :
-            self.path = name
-
-        # the url-path to the index.html file
-        self.html_path = self.path
-        
-        # dict of fname -> Folder
-        self.subdirs = {}
-
-        # dict of fname -> Image
-        self.images = {}
-        
-        # our human-friendly title
-        self.title = None
-
-        # our long-winded description
-        self.descr = ''
-
-        # is this folder non-empty?
-        self.alive = None
-        
-        # self.images.values(), but sorted by filename
-        self.sorted_images = []
-        
-        # the ShortURL key to this dir
-        self.shorturl_code = None
-
-        # were we filtered out?
-        self.filtered = False
-   
-    def pathFor (self, *fnames) :
-        """
-            Return a root-relative path to the given path inside this dir
-        """
-        return os.path.join(self.path, *fnames)
-
-    def index (self, filters=None) :
-        """
-            Look for other dirs and images inside this dir. Filters must be either None,
-            whereupon all files will be included, or a dict of {filename -> next_filter}.
-            If given, only filenames that are present in the dict will be indexed, and in
-            the case of dirs, the next_filter will be passed on to that Folder's index
-            method.
-        """
-
-        index.info("Indexing %s", self.path)
-
-        if filters :
-            self.filtered = True
-        
-        # iterate through listdir
-        for fname in os.listdir(self.path) :
-            # the full filesystem path to it
-            fpath = self.pathFor(fname)
-            
-            # ignore dotfiles
-            if fname.startswith('.') :
-                index.debug("Skipping dotfile %s", fname)
-                continue
-            
-            # apply filters
-            if filters :
-                if fname in filters :
-                    next_filter = filters[fname]
-                else :
-                    index.debug("Skip `%s' as we have a filter", fname)
-                    continue
-            else :
-                next_filter = None
-                
-            # recurse into subdirs, but not thumbs/previews
-            if os.path.isdir(fpath) and fname not in (THUMB_DIR, PREVIEW_DIR) :
-                index.debug("Found subdir %s", fpath)
-                f = self.subdirs[fname] = Folder(fname, self)
-                if f.index(next_filter) :   # recursion
-                    # if a subdir is alive, we are alive as well
-                    self.alive = True
-
-            # handle images
-            elif os.path.isfile(fpath) and isImage(fname) :
-                index.debug("Found image %s", fname)
-                self.images[fname] = Image(self, fname)
-
-            # ignore everything else
-            else :
-                index.debug("Ignoring file %s", fname)
-        
-        # sort and link the images
-        if self.images :
-            self.alive = True
-
-            # sort the images
-            fnames = self.images.keys()
-            fnames.sort()
-
-            prev = None
-
-            # link
-            for fname in fnames :
-                img = self.images[fname]
-
-                img.prev = prev
-
-                if prev :
-                    prev.next = img
-
-                prev = img
-                
-                # add to the sorted images list
-                self.sorted_images.append(img)
-
-        return self.alive
-
-    def getObjInfo (self) :
-        """
-            Metadata for shorturls2.db
-        """
-        return 'dir', self.path, 'index'
-
-    def linkTag (self) :
-        """
-            A text-link to this dir
-        """
-
-        return link(self.path, self.title)
-
-    def breadcrumb (self) :
-        """
-            Returns a [(fname, title)] list of this dir's parent dirs
-        """
-
-        f = self
-        b = []
-        d = 0
-        
-        while f :
-            b.insert(0, (dirUp(d), f.title))
-
-            d += 1
-            f = f.parent
-        
-        return b
-
-    def inRoot (self, *fnames) :
-        """
-            Return a relative URL from this dir to the given path in the root dir
-        """
-
-        c = len(self.path.split('/')) - 1
-
-        return os.path.join(*((['..']*c) + list(fnames)))
-    
-    def _page_fname (self, page) :
-        assert page >= 0
-
-        if page > 0 :
-            return  'index_%d.html' % page
-        else :
-            return 'index.html'
-
-    def render (self) :
-        """
-            Render the index.html, Images, and recurse into subdirs
-        """
-        
-        # ded folders are skipped
-        if not self.alive :
-            render.info("Skipping dir %s", self.path)
-            return
-        
-        # if this dir's contents were filtered out, then we can't render the index.html, as we aren't aware of all the images in here
-        if self.filtered :
-            render.warning("Dir `%s' contents were filtered, so we won't render the gallery index again", self.path)
+from optparse import OptionParser
 
-        else :  
-            # create the thumb/preview dirs if needed
-            for dir in (THUMB_DIR, PREVIEW_DIR) :
-                path = self.pathFor(dir)
-
-                if not os.path.isdir(path) :
-                    render.info("Creating dir %s", path)
-                    os.mkdir(path)
-
-            # figure out our title
-            title_path = self.pathFor(TITLE_FILE)
-            
-            title, descr = readTitleDescr(title_path)
-
-            if title :
-                self.title = title
-                self.descr = descr
-            
-            # default title for the root dir
-            elif self.name == '.' :
-                self.title = 'Index'
-
-            else :
-                self.title = self.name
-
-            # sort the subdirs
-            subdirs = self.subdirs.items()
-            subdirs.sort()
-            
-            # generate the <a href=""></a>'s for the subdirs
-            subdir_linkTags = [link(f.name, f.title) for fname, f in subdirs if f.alive]
-            
-            # stick them into a list
-            if subdir_linkTags :
-                directories = "<ul>\n\t<li>%s</li>\n</ul>" % "</li>\n\t<li>".join(subdir_linkTags)
-            else :
-                directories = ''
-
-            render.info("Rendering %s", self.path)
-
-            # paginate!
-            images = self.sorted_images
-            image_count = len(images)
-            pages = []
-            
-            while images :
-                pages.append(images[:IMAGE_COUNT])
-                images = images[IMAGE_COUNT:]
-
-            pagination_required = len(pages) > 1
-
-            if pagination_required :
-                render.info("Index split into %d pages of %d images each", len(pages), IMAGE_COUNT)
-            
-            for cur_page, page in enumerate(pages) :
-                if pagination_required :
-                    pagination = "<ul>" # <li>Showing Images %d - %d of %d</li>" % (cur_page*IMAGE_COUNT, cur_page*IMAGE_COUNT+len(page), image_count)
-                    
-                    if cur_page > 0 :
-                        pagination += '<li><a href="%s">&laquo; Prev</a></li>' % (self._page_fname(cur_page - 1))
-                    else :
-                        pagination += '<li><span>&laquo; Prev</span></li>'
-
-                    for x in xrange(0, len(pages)) :
-                        if x == cur_page :
-                            pagination += '<li><strong>%s</strong></li>' % (x + 1)
-                        else :
-                            pagination += '<li><a href="%s">%s</a></li>' % (self._page_fname(x), x+1)
-
-                    if cur_page < len(pages) - 1 :
-                        pagination += '<li><a href="%s">Next &raquo;</a></li>' % (self._page_fname(cur_page + 1))
-                    else :
-                        pagination += '<li><span>Next &raquo;</span></li>'
-
-                    pagination += "</ul>"
-                    shorturl = "%s/%s" % (self.shorturl_code, cur_page+1)
-                else :
-                    pagination = ''
-                    shorturl = self.shorturl_code
-
-                # render to index.html
-                gallery_tpl.renderTo(self.pathFor(self._page_fname(cur_page)), 
-                    STYLE_URL=self.inRoot('style.css'),
-                    BREADCRUMB=" &raquo; ".join([link(u, t) for (u, t) in self.breadcrumb()]),
-                    TITLE=self.title,
-                    DIRECTORIES=directories,
-                    PAGINATION=pagination,
-                    CONTENT="".join([i.thumbImgTag() for i in page]),
-                    DESCR=self.descr,
-                    SHORTURL=self.inRoot('s', shorturl),
-                    SHORTURL_CODE=shorturl,
-                )
-        
-        # render images
-        for img in self.images.itervalues() :
-            img.render()
-        
-        # recurse into subdirs
-        for dir in self.subdirs.itervalues() :
-            dir.render()
-                    
-class Image (object) :
-    def __init__ (self, dir, name) :
-        # the image filename, e.g. DSC3948.JPG
-        self.name = name
-
-        # the Folder object that we are in
-        self.dir = dir
-        
-        # the relative path from the root to us
-        self.path = dir.pathFor(name)
-
-        # the basename+ext, e.g. DSCR3948, .JPG
-        self.base_name, self.ext = os.path.splitext(name)
-        
-        # the root-relative paths to the thumb and preview images
-        self.thumb_path = self.dir.pathFor(THUMB_DIR, self.name)
-        self.preview_path = self.dir.pathFor(PREVIEW_DIR, self.name)
-        
-        # our user-friendly title
-        self.title = name
-
-        # our long-winded description
-        self.descr = ''
-
-        # the image before and after us, both may be None
-        self.prev = self.next = None
-        
-        # the name of the .html gallery view thing for this image, *always* self.name + ".html"
-        self.html_name = self.name + ".html"
-
-        # the root-relative path to the gallery view
-        self.html_path = self.dir.pathFor(self.html_name)
-        
-        #
-        # Figured out after prepare
-        #
-
-        # (w, h) tuple
-        self.img_size = None
-        
-        # the ShortURL code for this image
-        self.shorturl_code = None
-
-        # what to use in the rendered templates, intended to be overridden by subclasses
-        self.series_act = "add"
-        self.series_verb = "Add to"
-    
-    def getObjInfo (self) :
-        """
-            Metadata for shorturl2.db
-        """
-        return 'img', self.dir.path, self.name
-
-    def thumbImgTag (self) :
-        """
-            a <a><img /></a> of this image's thumbnail. Path relative to directory we are in
-        """
-        return link(self.html_name, "<img src='%s' alt='%s' title='%s' />" % (os.path.join(THUMB_DIR, self.name), self.descr, self.title))
+from lib import folder, shorturl
 
-    def previewImgTag (self) :
-        """
-            a <a><img /></a> of this image's preview. Path relative to directory we are in
-        """
-        return link(self.name, "<img src='%s' alt='%s' title='%s' />" % (os.path.join(PREVIEW_DIR, self.name), self.descr, self.title))
-
-    def linkTag (self) :
-        """
-            a <a></a> text-link to this image
-        """
-        return link(self.html_name, self.title)
-
-    def breadcrumb (self) :
-        """
-            Returns a [(fname, title)] list of this image's parents
-        """
-
-        f = self.dir
-        b = [(self.html_name, self.title)]
-        d = 0
-        
-        while f :
-            b.insert(0, (dirUp(d), f.title))
-
-            d += 1
-            f = f.parent
-        
-        return b
-
-    def render (self) :
-        """
-            Write out the .html file
-        """
-        
-        # stat the image file to get the filesize and mtime
-        st = os.stat(self.path)
-
-        self.filesize = st.st_size
-        self.timestamp = st.st_mtime
-        
-        # open the image in PIL to get image attributes + generate thumbnails
-        img = PIL.Image.open(self.path)
-
-        self.img_size = img.size
-
-        for out_path, geom in ((self.thumb_path, THUMB_GEOM), (self.preview_path, PREVIEW_GEOM)) :
-            # if it doesn't exist, or it's older than the image itself, generate
-            if not (os.path.exists(out_path) and os.stat(out_path).st_mtime > self.timestamp) :
-                render.info("Create thumbnailed image at %s with geom %s", out_path, geom)
-                
-                # XXX: is this the most efficient way to do this?
-                out_img = img.copy()
-                out_img.thumbnail(geom, resample=True)
-                out_img.save(out_path)
-        
-        # look for the metadata file
-        title_path = self.dir.pathFor(self.base_name + '.txt')
-        
-        title, descr = readTitleDescr(title_path)
-
-        if title :
-            self.title = title
-            self.descr = descr
-
-        render.info("Rendering image %s", self.path)
-
-        image_tpl.renderTo(self.html_path,
-            STYLE_URL=self.dir.inRoot('style.css'),
-            UP_URL=('.'),
-            PREV_URL=(self.prev and self.prev.html_name or ''),
-            NEXT_URL=(self.next and self.next.html_name or ''),
-            FILE=self.name,
-            BREADCRUMB=" &raquo; ".join([link(u, t) for u, t in self.breadcrumb()]),
-            TITLE=self.title,
-            PREVIOUS_THUMB=(self.prev and self.prev.thumbImgTag() or ''),
-            IMAGE=self.previewImgTag(),
-            NEXT_THUMB=(self.next and self.next.thumbImgTag() or ''),
-            DESCRIPTION=self.descr,
-            IMGSIZE="%dx%d" % self.img_size,
-            FILESIZE=fmtFilesize(self.filesize),
-            TIMESTAMP=fmtTimestamp(self.timestamp),
-            SHORTURL=self.dir.inRoot('s', self.shorturl_code),
-            SHORTURL_CODE=self.shorturl_code,
-            SERIES_URL=self.dir.inRoot('series/%s/%s' % (self.series_act, self.shorturl_code)),
-            SERIES_VERB=self.series_verb,
-        )   
-    
-    def __str__ (self) :
-        return "Image `%s' in `%s'" % (self.name, self.dir.path)
-
-def int2key (id) :
-    """
-        Turn an integer into a short-as-possible url-safe string
-    """
-    for type in ('B', 'H', 'I') :
-        try :
-            return base64.b64encode(struct.pack(type, id), '-_').rstrip('=')
-        except struct.error :
-            continue
-
-    raise Exception("ID overflow: %s" % id)
-
-def updateShorturlDb (root) :
-    """
-        DeGAL <= 0.2 used a simple key => path mapping, but now we use
-        something more structured, key => (type, dirpath, fname), where
-
-        type    - one of 'img', 'dir'
-        dirpath - the path to the directory, e.g. '.', './foobar', './foobar/quux'
-        fname   - the filename, one of '', 'DSC9839.JPG', 'this.png', etc.
-    """
-
-    db = shelve.open('shorturls2', 'c', writeback=True)
-    
-    id = db.get('_id', 1)
-
-    dirqueue = [root]
-
-    # dict of path -> obj
-    paths = {}
-
-    index.info("Processing ShortURLs...")
-
-    while dirqueue :
-        dir = dirqueue.pop(0)
-
-        dirqueue.extend(dir.subdirs.itervalues())
-
-        if dir.alive :
-            paths[dir.path] = dir
-
-        for img in dir.images.itervalues() :
-            paths[img.path] = img
-
-    for key in db.keys() :
-        if key.startswith('_') :
-            continue
-
-        type, dirpath, fname = db[key]
-        
-        path = os.path.join(dirpath, fname).rstrip('/')
-
-        try :
-            paths.pop(path).shorturl_code = key
-            index.debug("Code for `%s' is %s", path, key)
-
-        except KeyError :
-            index.debug("Path `%s' in DB does not exist?", path)
-
-    for obj in paths.itervalues() :
-        key = int2key(id)
-        id += 1
-        
-        index.info("Alloc code `%s' for `%s'", key, obj.html_path)
-
-        obj.shorturl_code = key
-
-        db[key] = obj.getObjInfo()
-
-    db['_id'] = id
-    db.close()
-
-def main (targets=()) :
+def main (dir='.', targets=()) :
     root_filter = {}
 
     for target in targets :
@@ -670,24 +34,19 @@
             if path_part :
                 if path_part not in f :
                     f[path_part] = {}
+                    
                 f = f[path_part]
-    
-    index.debug('Filter: %s', root_filter)
 
-    root = Folder()
+    root = folder.Folder(dir)
     root.index(root_filter)
-    updateShorturlDb(root)
+    shorturl.updateDB(root)
     root.render()
 
-def fmtFilesize (size) :
-    return utils.formatbytes(size, forcekb=False, largestonly=True, kiloname='KiB', meganame='MiB', bytename='B', nospace=False)
-
-def fmtTimestamp (ts) :
-    return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
-
 if __name__ == '__main__' :
-    from sys import argv
-    argv.pop(0)
-
-    main(argv)
-
+    parser = OptionParser(usage="usage: %prog [options] ... [target ...]")
+    
+    parser.add_option("-d", "--dir", dest="dir", help="look for images in DIR and write the HTML there", metavar="DIR", default=".")
+    
+    options, filter_targets = parser.parse_args()
+    
+    main(options.dir, filter_targets)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/folder.py	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,287 @@
+import os, os.path
+
+import settings, image, utils
+from log import index, render
+from template import gallery as gallery_tpl
+from helpers import url_for_page
+
+def dirUp (count=1) :
+    """
+        Returns a relative path to the directly count levels above the current one
+    """
+
+    if not count :
+        return '.'
+
+    return os.path.join(*(['..']*count))
+    
+def isImage (fname) :
+    """
+        Is the given filename likely to be an image file?
+    """
+
+    fname = fname.lower()
+    base, ext = os.path.splitext(fname)
+    ext = ext.lstrip('.')
+
+    return ext in settings.IMAGE_EXTS
+
+class Folder (object) :
+    def __init__ (self, name='.', parent=None) :
+        # the directory name, no trailing /
+        self.name = name.rstrip(os.sep)
+
+        # our parent Folder, or None
+        self.parent = parent
+
+        # the path to this dir, as a relative path to the root of the image gallery, always starts with .
+        if parent and name :
+            self.path = parent.pathFor(self.name)
+        else :
+            self.path = self.name
+
+        # the url-path to the index.html file
+        self.html_path = self.path
+        
+        # dict of fname -> Folder
+        self.subdirs = {}
+
+        # dict of fname -> Image
+        self.images = {}
+        
+        # our human-friendly title
+        self.title = None
+
+        # our long-winded description
+        self.descr = ''
+
+        # is this folder non-empty?
+        self.alive = None
+        
+        # self.images.values(), but sorted by filename
+        self.sorted_images = []
+        
+        # the ShortURL key to this dir
+        self.shorturl_code = None
+
+        # were we filtered out?
+        self.filtered = False
+   
+    def pathFor (self, *fnames) :
+        """
+            Return a root-relative path to the given path inside this dir
+        """
+        return os.path.join(self.path, *fnames)
+
+    def index (self, filters=None) :
+        """
+            Look for other dirs and images inside this dir. Filters must be either None,
+            whereupon all files will be included, or a dict of {filename -> next_filter}.
+            If given, only filenames that are present in the dict will be indexed, and in
+            the case of dirs, the next_filter will be passed on to that Folder's index
+            method.
+        """
+
+        index.info("Indexing %s", self.path)
+
+        if filters :
+            self.filtered = True
+        
+        # iterate through listdir
+        for fname in os.listdir(self.path) :
+            # the full filesystem path to it
+            fpath = self.pathFor(fname)
+            
+            # ignore dotfiles
+            if fname.startswith('.') :
+                index.debug("Skipping dotfile %s", fname)
+                continue
+            
+            # apply filters
+            if filters :
+                if fname in filters :
+                    next_filter = filters[fname]
+                else :
+                    index.debug("Skip `%s' as we have a filter", fname)
+                    continue
+            else :
+                next_filter = None
+                
+            # recurse into subdirs, but not thumbs/previews
+            if (os.path.isdir(fpath) 
+                and (fname not in (settings.THUMB_DIR, settings.PREVIEW_DIR))
+                and (self.parent or fname not in settings.ROOT_IGNORE)
+            ) :
+                index.debug("Found subdir %s", fpath)
+                f = self.subdirs[fname] = Folder(fname, self)
+                if f.index(next_filter) :   # recursion
+                    # if a subdir is alive, we are alive as well
+                    self.alive = True
+
+            # handle images
+            elif os.path.isfile(fpath) and isImage(fname) :
+                index.debug("Found image %s", fname)
+                self.images[fname] = image.Image(self, fname)
+
+            # ignore everything else
+            else :
+                index.debug("Ignoring file %s", fname)
+        
+        # sort and link the images
+        if self.images :
+            self.alive = True
+
+            # sort the images
+            fnames = self.images.keys()
+            fnames.sort()
+
+            prev = None
+
+            # link
+            for fname in fnames :
+                img = self.images[fname]
+
+                img.prev = prev
+
+                if prev :
+                    prev.next = img
+
+                prev = img
+                
+                # add to the sorted images list
+                self.sorted_images.append(img)
+                
+            # figure out our title/ descr. Must be done before our parent dir is rendered (self.title)
+            title_path = self.pathFor(settings.TITLE_FILE)
+            
+            self.title, self.descr = utils.readTitleDescr(title_path)
+            
+            # default title for the root dir
+            if self.title :
+                self.alive = True
+                pass # use what was in the title file
+                
+            elif not self.parent :
+                self.title = 'Index'
+
+            else :
+                self.title = self.name
+            
+            if self.descr :
+                self.alive = True
+
+        return self.alive
+
+    def getObjInfo (self) :
+        """
+            Metadata for shorturls2.db
+        """
+        return 'dir', self.path, ''
+
+    def breadcrumb (self) :
+        """
+            Returns a [(fname, title)] list of this dir's parent dirs
+        """
+
+        f = self
+        b = []
+        d = 0
+        
+        while f :
+            b.insert(0, (dirUp(d), f.title))
+
+            d += 1
+            f = f.parent
+        
+        return b
+
+    def countParents (self, acc=0) :
+        if self.parent :
+            return self.parent.countParents(acc+1)
+        else :
+            return acc
+    
+    def inRoot (self, *fnames) :
+        """
+            Return a relative URL from this dir to the given path in the root dir
+        """
+
+        c = self.countParents()
+
+        return utils.url_join(*((['..']*c) + list(fnames)))
+
+    def render (self) :
+        """
+            Render the index.html, Images, and recurse into subdirs
+        """
+        
+        # ded folders are skipped
+        if not self.alive :
+            render.info("Skipping dir %s (no images)", self.path)
+            return
+        
+        # if this dir's contents were filtered out, then we can't render the index.html, as we aren't aware of all the images in here
+        if self.filtered :
+            render.warning("Dir `%s' contents were filtered, so we won't render the gallery index again", self.path)
+
+        else :  
+            # create the thumb/preview dirs if needed
+            for dir in (settings.THUMB_DIR, settings.PREVIEW_DIR) :
+                path = self.pathFor(dir)
+
+                if not os.path.isdir(path) :
+                    render.info("Creating dir %s", path)
+                    os.mkdir(path)
+
+            # sort the subdirs
+            subdirs = self.subdirs.values()
+            subdirs.sort(key=lambda d: d.name)
+            
+            render.info("Rendering %s", self.path)
+
+            # paginate!
+            images = self.sorted_images
+            image_count = len(images)
+            pages = []
+            
+            while images :
+                pages.append(images[:settings.IMAGE_COUNT])
+                images = images[settings.IMAGE_COUNT:]
+
+            pagination_required = len(pages) > 1
+
+            if pagination_required :
+                render.info("Index split into %d pages of %d images each", len(pages), settings.IMAGE_COUNT)
+            
+            for cur_page, images in enumerate(pages) :
+                if pagination_required and cur_page > 0 :
+                    shorturl = "%s/%s" % (self.shorturl_code, cur_page+1)
+                else :
+                    shorturl = self.shorturl_code
+
+                # render to index.html
+                gallery_tpl.render_to(self.pathFor(url_for_page(cur_page)), 
+                    stylesheet_url               = self.inRoot('style.css'),
+                    title                        = self.title,
+                    breadcrumb                   = self.breadcrumb(),
+                    
+                    dirs                         = subdirs,
+                    images                       = images,
+                    
+                    num_pages                    = len(pages),
+                    cur_page                     = cur_page,
+                    
+                    description                  = self.descr,
+                    
+                    shorturl                     = self.inRoot('s', shorturl),
+                    shorturl_code                = shorturl,
+                )
+        
+        # render images
+        for img in self.images.itervalues() :
+            img.render()
+        
+        # recurse into subdirs
+        for dir in self.subdirs.itervalues() :
+            dir.render()
+                
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/formatbytes.py	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,116 @@
+###############################################################
+# Functions taken from pathutils.py Version 0.2.5 (2005/12/06), http://www.voidspace.org.uk/python/recipebook.shtml#utils
+# Copyright Michael Foord 2004
+# Released subject to the BSD License
+# Please see http://www.voidspace.org.uk/python/license.shtml
+
+###############################################################
+# formatbytes takes a filesize (as returned by os.getsize() )
+# and formats it for display in one of two ways !!
+
+# For information about bugfixes, updates and support, please join the Pythonutils mailing list.
+# http://groups.google.com/group/pythonutils/
+# Comments, suggestions and bug reports welcome.
+# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
+# E-mail fuzzyman@voidspace.org.uk
+
+def formatbytes(sizeint, configdict=None, **configs):
+    """
+    Given a file size as an integer, return a nicely formatted string that
+    represents the size. Has various options to control it's output.
+    
+    You can pass in a dictionary of arguments or keyword arguments. Keyword
+    arguments override the dictionary and there are sensible defaults for options
+    you don't set.
+    
+    Options and defaults are as follows :
+    
+    *    ``forcekb = False`` -         If set this forces the output to be in terms
+    of kilobytes and bytes only.
+    
+    *    ``largestonly = True`` -    If set, instead of outputting 
+        ``1 Mbytes, 307 Kbytes, 478 bytes`` it outputs using only the largest 
+        denominator - e.g. ``1.3 Mbytes`` or ``17.2 Kbytes``
+    
+    *    ``kiloname = 'Kbytes'`` -    The string to use for kilobytes
+    
+    *    ``meganame = 'Mbytes'`` - The string to use for Megabytes
+    
+    *    ``bytename = 'bytes'`` -     The string to use for bytes
+    
+    *    ``nospace = True`` -        If set it outputs ``1Mbytes, 307Kbytes``, 
+        notice there is no space.
+    
+    Example outputs : ::
+    
+        19Mbytes, 75Kbytes, 255bytes
+        2Kbytes, 0bytes
+        23.8Mbytes
+    
+    .. note::
+    
+        It currently uses the plural form even for singular.
+    """
+    defaultconfigs = {  'forcekb' : False,
+                        'largestonly' : True,
+                        'kiloname' : 'Kbytes',
+                        'meganame' : 'Mbytes',
+                        'bytename' : 'bytes',
+                        'nospace' : True}
+    if configdict is None:
+        configdict = {}
+    for entry in configs:
+        # keyword parameters override the dictionary passed in
+        configdict[entry] = configs[entry]
+    #
+    for keyword in defaultconfigs:
+        if not configdict.has_key(keyword):
+            configdict[keyword] = defaultconfigs[keyword]
+    #
+    if configdict['nospace']:
+        space = ''
+    else:
+        space = ' '
+    #
+    mb, kb, rb = bytedivider(sizeint)
+    if configdict['largestonly']:
+        if mb and not configdict['forcekb']:
+            return stringround(mb, kb)+ space + configdict['meganame']
+        elif kb or configdict['forcekb']:
+            if mb and configdict['forcekb']:
+                kb += 1024*mb
+            return stringround(kb, rb) + space+ configdict['kiloname']
+        else:
+            return str(rb) + space + configdict['bytename']
+    else:
+        outstr = ''
+        if mb and not configdict['forcekb']:
+            outstr = str(mb) + space + configdict['meganame'] +', '
+        if kb or configdict['forcekb'] or mb:
+            if configdict['forcekb']:
+                kb += 1024*mb 
+            outstr += str(kb) + space + configdict['kiloname'] +', '
+        return outstr + str(rb) + space + configdict['bytename']
+
+def stringround(main, rest):
+    """
+    Given a file size in either (mb, kb) or (kb, bytes) - round it
+    appropriately.
+    """
+    # divide an int by a float... get a float
+    value = main + rest/1024.0
+    return str(round(value, 1))
+
+def bytedivider(nbytes):
+    """
+    Given an integer (probably a long integer returned by os.getsize() )
+    it returns a tuple of (megabytes, kilobytes, bytes).
+    
+    This can be more easily converted into a formatted string to display the
+    size of the file.
+    """ 
+    mb, remainder = divmod(nbytes, 1048576)
+    kb, rb = divmod(remainder, 1024)
+    return (mb, kb, rb)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/helpers.py	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,31 @@
+# template helper functions
+import urllib
+from formatbytes import formatbytes
+from datetime import datetime
+
+def iter_is_first (seq) :
+    flag = True
+    
+    for item in seq :
+        yield item, flag
+        flag = False
+        
+def url_for_page (page) :
+    assert page >= 0
+
+    if page > 0 :
+        return  'index_%d.html' % page
+    else :
+        return 'index.html'
+
+def tag_for_img (page, img) :
+    return """<a href="%s"><img src="%s" /></a>""" % (page, img)
+
+def format_filesize (size) :
+    return formatbytes(size, forcekb=False, largestonly=True, kiloname='KiB', meganame='MiB', bytename='B', nospace=False)
+
+def format_timestamp (ts) :
+    return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
+
+def format_imgsize (size) :
+    return "%dx%d" % size
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/image.py	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,129 @@
+import os, os.path
+
+import PIL.Image
+
+import settings, utils
+from log import index, render
+from template import image as image_tpl
+    
+class Image (object) :
+    def __init__ (self, dir, name) :
+        # the image filename, e.g. DSC3948.JPG
+        self.name = name
+
+        # the Folder object that we are in
+        self.dir = dir
+        
+        # the relative path from the root to us
+        self.path = dir.pathFor(name)
+
+        # the basename+ext, e.g. DSCR3948, .JPG
+        self.base_name, self.ext = os.path.splitext(name)
+        
+        # our user-friendly title
+        self.title = name
+
+        # our long-winded description
+        self.descr = ''
+
+        # the image before and after us, both may be None
+        self.prev = self.next = None
+        
+        # the image-relative names for the html page, thumb and preview images
+        self.html_name = self.name + ".html"
+        self.thumb_name = utils.url_join(settings.THUMB_DIR, self.name)
+        self.preview_name = utils.url_join(settings.PREVIEW_DIR, self.name)
+
+        # the root-relative paths to the html page, thumb and preview images
+        self.html_path = self.dir.pathFor(self.html_name)
+        self.thumb_path = self.dir.pathFor(settings.THUMB_DIR, self.name)
+        self.preview_path = self.dir.pathFor(settings.PREVIEW_DIR, self.name)        
+        
+        #
+        # Figured out after prepare
+        #
+
+        # (w, h) tuple
+        self.img_size = None
+        
+        # the ShortURL code for this image
+        self.shorturl_code = None
+
+        # what to use in the rendered templates, intended to be overridden by subclasses
+        self.series_act = "add"
+        self.series_verb = "Add to"
+    
+    def getObjInfo (self) :
+        """
+            Metadata for shorturl2.db
+        """
+        return 'img', self.dir.path, self.name
+
+    def breadcrumb (self) :
+        """
+            Returns a [(fname, title)] list of this image's parents
+       """
+        
+        return self.dir.breadcrumb() + [(self.html_name, self.title)]
+
+    def render (self) :
+        """
+            Write out the .html file
+        """
+        
+        # stat the image file to get the filesize and mtime
+        st = os.stat(self.path)
+
+        self.filesize = st.st_size
+        self.timestamp = st.st_mtime
+        
+        # open the image in PIL to get image attributes + generate thumbnails
+        img = PIL.Image.open(self.path)
+
+        self.img_size = img.size
+
+        for out_path, geom in ((self.thumb_path, settings.THUMB_GEOM), (self.preview_path, settings.PREVIEW_GEOM)) :
+            # if it doesn't exist, or it's older than the image itself, generate
+            if not (os.path.exists(out_path) and os.stat(out_path).st_mtime > self.timestamp) :
+                render.info("Create thumbnailed image at %s with geom %s", out_path, geom)
+                
+                # XXX: is this the most efficient way to do this? It seems slow
+                out_img = img.copy()
+                out_img.thumbnail(geom, resample=True)
+                out_img.save(out_path)
+        
+        # look for the metadata file
+        title_path = self.dir.pathFor(self.base_name + '.txt')
+        
+        self.title, self.descr = utils.readTitleDescr(title_path)
+        
+        if not self.title :
+            self.title = self.name
+
+        render.info("Rendering image %s", self.path)
+
+        image_tpl.render_to(self.html_path,
+            stylesheet_url             = self.dir.inRoot('style.css'),
+            title                      = self.title,
+            breadcrumb                 = self.breadcrumb(),
+            
+            prev                       = self.prev,
+            next                       = self.next,
+            img                        = self,
+            
+            description                = self.descr,
+            
+            filename                   = self.name,
+            img_size                   = self.img_size,
+            file_size                  = self.filesize,
+            timestamp                  = self.timestamp,
+            
+            shorturl                   = self.dir.inRoot('s', self.shorturl_code),
+            shorturl_code              = self.shorturl_code,
+            
+            series_url                 = self.dir.inRoot('series/%s/%s' % (self.series_act, self.shorturl_code)),
+            series_verb                = self.series_verb,
+        )   
+    
+    def __str__ (self) :
+        return "Image `%s' in `%s'" % (self.name, self.dir.path)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/log.py	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,14 @@
+import logging
+
+logging.basicConfig(
+    level=logging.DEBUG,
+    format="%(message)s",
+#    format="%(name)8s %(levelname)8s   %(lineno)3d %(message)s",
+
+)
+
+index = logging.getLogger('index')
+render = logging.getLogger('render')
+template = logging.getLogger('template')
+
+template.setLevel(logging.ERROR)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/settings.py	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,19 @@
+TEMPLATE_DIR = './templates'
+TEMPLATE_EXT = 'html'
+
+IMAGE_EXTS = ('jpg', 'jpeg', 'png', 'gif', 'bmp')
+
+THUMB_DIR = 'thumbs'
+PREVIEW_DIR = 'previews'
+TITLE_FILE = 'title.txt'
+
+THUMB_GEOM = (160, 120)
+PREVIEW_GEOM = (640, 480)
+
+DEFAULT_TITLE = 'Image gallery'
+
+# how many image/page
+IMAGE_COUNT = 50
+
+VERSION = "0.5"
+ROOT_IGNORE = ('lib', 'templates')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/shorturl.py	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,78 @@
+import struct
+import base64
+import shelve
+import os.path
+
+from log import index
+
+def int2key (id) :
+    """
+        Turn an integer into a short-as-possible url-safe string
+    """
+    for type in ('B', 'H', 'I') :
+        try :
+            return base64.b64encode(struct.pack(type, id), '-_').rstrip('=')
+        except struct.error :
+            continue
+
+    raise Exception("ID overflow: %s" % id)
+
+def updateDB (root) :
+    """
+        DeGAL <= 0.2 used a simple key => path mapping, but now we use
+        something more structured, key => (type, dirpath, fname), where
+
+        type    - one of 'img', 'dir'
+        dirpath - the path to the directory, e.g. '.', './foobar', './foobar/quux'
+        fname   - the filename, one of '', 'DSC9839.JPG', 'this.png', etc.
+    """
+
+    db = shelve.open('shorturls2', 'c', writeback=True)
+    
+    id = db.get('_id', 1)
+
+    dirqueue = [root]
+
+    # dict of path -> obj
+    paths = {}
+
+    index.info("Processing ShortURLs...")
+
+    while dirqueue :
+        dir = dirqueue.pop(0)
+
+        dirqueue.extend(dir.subdirs.itervalues())
+
+        if dir.alive :
+            paths[dir.path] = dir
+
+        for img in dir.images.itervalues() :
+            paths[img.path] = img
+
+    for key in db.keys() :
+        if key.startswith('_') :
+            continue
+
+        type, dirpath, fname = db[key]
+        
+        path = os.path.join(dirpath, fname).rstrip(os.sep)
+
+        try :
+            paths.pop(path).shorturl_code = key
+            index.debug("Code for `%s' is %s", path, key)
+
+        except KeyError :
+            index.debug("Path `%s' in DB does not exist?", path)
+
+    for obj in paths.itervalues() :
+        key = int2key(id)
+        id += 1
+        
+        index.info("Alloc code `%s' for `%s'", key, obj.html_path)
+
+        obj.shorturl_code = key
+
+        db[key] = obj.getObjInfo()
+
+    db['_id'] = id
+    db.close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/template.py	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,45 @@
+from mako import exceptions
+from mako.lookup import TemplateLookup
+
+import settings, helpers
+
+import log
+
+_lookup = TemplateLookup(
+    directories=[settings.TEMPLATE_DIR], 
+    module_directory='%s/cache' % settings.TEMPLATE_DIR, 
+    output_encoding='utf-8',
+    filesystem_checks=False,        # this may need to be changed if used in a long-term process
+)
+
+TEMPLATE_GLOBALS = dict(
+    h                          = helpers,
+    version                    = settings.VERSION,
+)
+
+class Template (object) :
+    def __init__ (self, name) :
+        self.name = name
+        self.tpl = _lookup.get_template("%s.%s" % (name, settings.TEMPLATE_EXT))
+    
+    def render (self, **data) :
+        data.update(TEMPLATE_GLOBALS)
+        
+        try :
+            log.template.debug("render %s with %s", self.name, data)
+            return self.tpl.render(**data)
+        except :
+            data = exceptions.text_error_template().render()
+            log.template.error(data)
+            
+            raise
+    
+    def render_to (self, file, **data) :
+        fh = open(file, "w")
+        fh.write(self.render(**data))
+        fh.close()
+    
+# templates
+gallery = Template("gallery")
+image = Template("image")
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/utils.py	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,38 @@
+import os.path
+
+def readFile (path) :
+    fo = open(path, 'r')
+    data = fo.read()
+    fo.close()
+
+    return data
+
+def readTitleDescr (path) :
+    """
+        Read a title.txt or <imgname>.txt file
+    """
+
+    if os.path.exists(path) :
+        content = readFile(path)
+
+        if '---' in content :
+            title, descr = content.split('---', 1)
+        else :
+            title, descr = content, ''
+
+        return title.strip(), descr.strip()
+
+    return "", ""
+
+def url_join (*parts) :
+    return '/'.join(parts)
+ 
+from optparse import OptionParser
+    
+def optparse (options) :
+    parser = OptionParser()
+    
+    
+    parser.add_option    
+    
+    
\ No newline at end of file
--- a/series.cgi	Thu Dec 20 17:42:04 2007 +0000
+++ b/series.cgi	Fri Dec 21 20:36:03 2007 +0000
@@ -1,4 +1,24 @@
 #!/usr/bin/env python2.4
+#
+# DeGAL - A pretty simple web image gallery
+# Copyright (C) 2007 Tero Marttila
+# http://marttila.de/~terom/degal/
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the
+# Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
 
 import shelve
 import cgi
--- a/shorturl.cgi	Thu Dec 20 17:42:04 2007 +0000
+++ b/shorturl.cgi	Fri Dec 21 20:36:03 2007 +0000
@@ -1,4 +1,25 @@
-#!/usr/bin/env python2.5
+#!/usr/bin/env python2.4
+#
+# DeGAL - A pretty simple web image gallery
+# Copyright (C) 2007 Tero Marttila
+# http://marttila.de/~terom/degal/
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the
+# Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+
 
 import shelve
 import cgi
--- a/taggr.cgi	Thu Dec 20 17:42:04 2007 +0000
+++ b/taggr.cgi	Fri Dec 21 20:36:03 2007 +0000
@@ -1,4 +1,25 @@
 #!/usr/bin/env python2.4
+#
+# DeGAL - A pretty simple web image gallery
+# Copyright (C) 2007 Tero Marttila
+# http://marttila.de/~terom/degal/
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the
+# Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+
 
 import cgi
 import shelve
--- a/templates/gallery.html	Thu Dec 20 17:42:04 2007 +0000
+++ b/templates/gallery.html	Fri Dec 21 20:36:03 2007 +0000
@@ -1,25 +1,56 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
-  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<%inherit file="master.html" /> <!-- %> -->
 
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-  <head>
-    <title><!-- TITLE --></title>
-    <link rel="Stylesheet" type="text/css" href="<!-- STYLE_URL -->" />
-  </head>
-  <body>
-    <div id="breadcrumb"><!-- BREADCRUMB --></div>
-    <h1><!-- TITLE --></h1>
-    <div id="dirs"><!-- DIRECTORIES --></div>
-    <div class="paginate"><!-- PAGINATION --></div>
+<%def name="pagination(num_pages, cur_page)"> <!-- %> -->
+% if num_pages > 1 :
+        <ul>
+        
+%   if cur_page > 0 :
+            <li><a href="${h.url_for_page(cur_page - 1)}">&laquo; Prev</a></li>
+%   else :
+            <li><span>&laquo; Prev</span></li>
+%   endif
+
+%   for page in xrange(0, num_pages) :
+%     if page == cur_page :
+            <li><strong>${page + 1}</strong></li>
+%     else :
+            <li><a href="${h.url_for_page(page)}">${page + 1}</a></li>
+%     endif            
+%   endfor
+
+%   if cur_page < num_pages - 1 :
+            <li><a href="${h.url_for_page(cur_page + 1)}">Next &raquo;</a></li>
+%   else :
+            <li><span>Next &raquo;</span></li>
+%   endif
+        </ul>
+% endif       
+</%def> <!-- %> -->
+
+    <h1>${title}</h1>
+    <div id="dirs">
+% if dirs :
+        <ul>
+%   for dir in dirs :
+            <li><a href="${dir.name}">${dir.title}</a></li>
+%   endfor
+        </ul>
+% endif
+    </div>
+    <div class="paginate">
+${pagination(num_pages, cur_page)}
+    </div>
     <div id="thumbnails">
-      <!-- CONTENT -->
+% for img in images :
+        ${h.tag_for_img(img.html_name, img.thumb_name)}
+% endfor
     </div>
-    <div class="paginate"><!-- PAGINATION --></div>
-    <p id="description"><!-- DESCR --></p>
+    <div class="paginate">
+${pagination(num_pages, cur_page)}
+    </div>
+    <p id="description">
+${description}
+    </p>
     <div id="info">
-        <p>ShortURL: <a href="<!-- SHORTURL -->" rel="nofollow"><!-- SHORTURL_CODE --></a></p>
+        <p>ShortURL: <a href="${shorturl}>" rel="nofollow">${shorturl_code}</a></p>
     </div>
-    <p id="about"><a href="http://marttila.de/~terom/degal/">DeGAL</a> <!-- VERSION --></p>
-  </body>
-</html>
--- a/templates/image.html	Thu Dec 20 17:42:04 2007 +0000
+++ b/templates/image.html	Fri Dec 21 20:36:03 2007 +0000
@@ -1,30 +1,27 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
-  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<%inherit file="master.html" /> <!-- %> -->
 
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-  <head>
-    <title><!-- TITLE --></title>
-    <link rel="Stylesheet" type="text/css" href="<!-- STYLE_URL -->" />
-    <link rel="up" href="<!-- UP_URL -->" />
-    <link rel="prev" href="<!-- PREV_URL -->" />
-    <link rel="next" href="<!-- NEXT_URL -->" />
-  </head>
-  <body>
-    <div id="breadcrumb"><!-- BREADCRUMB --></div>
     <div id="image">
-      <h1><!-- TITLE --></h1>
-      <p><!-- PREVIOUS_THUMB --> <!-- IMAGE --> <!-- NEXT_THUMB --></p>
-      <p><!-- DESCRIPTION --></p>
+        <h1>${title}</h1>
+        <p>
+% if prev :        
+            ${h.tag_for_img(prev.html_name, prev.thumb_name)}
+% endif
+            
+            ${h.tag_for_img(img.html_name, img.preview_name)}
+            
+% if next :            
+            ${h.tag_for_img(next.html_name, next.thumb_name)}
+% endif
+        </p>
+        <p>
+            ${description}
+        </p>
     </div>
     <div id="info">
-      <p><!-- FILE --></p>
-      <p><!-- IMGSIZE --></p>
-      <p><!-- FILESIZE --></p>
-      <p><!-- TIMESTAMP --></p>
-      <p>ShortURL: <a href="<!-- SHORTURL -->" rel="nofollow"><!-- SHORTURL_CODE --></a></p>
-      <p><a href="<!-- SERIES_URL -->" rel="nofollow"><!-- SERIES_VERB --></a> series</p>
+      <p>${filename}</p>
+      <p>${h.format_imgsize(img_size)}</p>
+      <p>${h.format_filesize(file_size)}</p>
+      <p>${h.format_timestamp(timestamp)}</p>
+      <p>ShortURL: <a href="${shorturl}" rel="nofollow">${shorturl_code}</a></p>
+      <p><a href="${series_url}" rel="nofollow">${series_verb}</a> series</p>
     </div>
-    <p id="about"><a href="http://marttila.de/~terom/degal/">DeGAL</a> <!-- VERSION --></p>
-  </body>
-</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/master.html	Fri Dec 21 20:36:03 2007 +0000
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+  <head>
+    <title>${title}</title>
+    <link rel="Stylesheet" type="text/css" href="${stylesheet_url}" />
+  </head>
+  <body>
+    <div id="breadcrumb">
+% for (( bc_url, bc_title), is_first) in h.iter_is_first(breadcrumb) :
+%   if not is_first :
+        &raquo;
+%   endif
+        <a href="${bc_url}">${bc_title}</a>
+% endfor
+    </div>
+    ${next.body()}
+    <p id="about"><a href="http://marttila.de/~terom/degal/">DeGAL</a> ${version}</p>
+  </body>
+</html>