View the Source Code


Run unit tests

views.py

"""
Monomotapa - A Micro CMS
Copyright (C) 2014, Paul Munday.

PO Box 28228, Portland, OR, USA 97228
paul at paulmunday.net

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero  Public License as published by
the Free Software Foundation, either version 3 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 Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

See http://paulmunday.net/license for licensing details.

Monomotapa:
    a city whose inhabitants are bounded by deep feelings of friendship,
    so thatthey intuit one another's most secret needs and desire. For
    instance, if one dreams that his friend is sad, the friend will perceive
    the distress and rush to the sleepers rescue.

    (Jean de La Fontaine, *Fables choisies, mises en vers*, VIII:11 Paris,
    2nd ed., 1678-9)

cited in :
Alberto Manguel and Gianni Guadalupi, *The Dictionary of Imaginary Places*,
Bloomsbury, London, 1999.

A micro app written using the Flask microframework to manage my personal site.
It is designed so that publishing a page requires no more than dropping a
markdown page in the appropriate directory (though you need to edit a json file
if you want it to appear in the top navigation).

It can also display its own source code and run its own unit tests.

The name 'monomotapa' was chosen more or less at random (it shares an initial
with me) as I didn't want to name it after the site and be typing import
paulmunday, or something similar, as that would be strange.
"""

# pylint: disable=no-name-in-module, redefined-outer-name
from flask import render_template, abort, Markup, escape, request
from flask import make_response

from pygments import highlight
from pygments.lexers import PythonLexer, HtmlDjangoLexer, TextLexer
from pygments.formatters import HtmlFormatter

import markdown

import os.path
import os
import json
import random
import subprocess
import tempfile
from collections import OrderedDict

from monomotapa import app
from monomotapa.config import ConfigError


class MonomotapaError(Exception):
    """create classs for own errors"""
    pass


def get_page_attributes(jsonfile):
    """Returns dictionary of page_attributes.
    Defines additional static page attributes loaded from json file.
    N.B. static pages do not need to have attributes defined there,
    it is sufficient to have a page.md in src for each /page
    possible values are src (name of markdown file to be rendered)
    heading, title, and trusted (i.e. allow embeded html in markdown)"""
    try:
        with open(src_file(jsonfile), 'r') as pagesfile:
            page_attributes = json.load(pagesfile)
    except IOError:
        page_attributes = []
    return page_attributes


def get_page_attribute(attr_src, page, attribute):
    """returns attribute of page if it exists, else None.
    attr_src = dictionary(from get_page_attributes)"""
    if page in attr_src and attribute in attr_src[page]:
        return attr_src[page][attribute]
    else:
        return None


# Navigation
def top_navigation(page):
    """Generates navigation as an OrderedDict from navigation.json.
    Navigation.json consists of a json array(list) "nav_order"
    containing the names of the top navigation elements and
    a json object(dict) called "nav_elements"
    if a page is to show up in the top navigation
    there must be an entry present in nav_order but there need not
    be one in nav_elements. However if there is the key must be the same.
    Possible values for nav_elements are link_text, url and urlfor
    The name  from nav_order will be used to set the link text,
    unless link_text is present in nav_elements.
    url and urlfor are optional, however if ommited the url wil be
    generated in the navigation by  url_for('staticpage', page=[key])
    equivalent to  @app.route"/page"; def page())
    which may not be correct. If a url is supplied  it will be used
    otherwise if urlfor is supplied it the url will be
    generated with url_for(urlfor). url takes precendence so it makes
    no sense to supply both.
    Web Sign-in is supported by adding a "rel": "me" attribute.
    """
    with open(src_file('navigation.json'), 'r') as navfile:
        navigation = json.load(navfile)
    base_nav = OrderedDict({})
    for key in navigation["nav_order"]:
        nav = {}
        nav['base'] = key
        nav['link_text'] = key
        if key in navigation["nav_elements"]:
            elements = navigation["nav_elements"][key]
            nav.update(elements)
        base_nav[key] = nav
    return {'navigation':  base_nav, 'page': page}


# For pages
class Page(object):
    """Generates  pages as objects"""
    # pylint: disable=too-many-instance-attributes
    def __init__(self, page, **kwargs):
        """Define attributes for  pages (if present).
        Sets self.name, self.title, self.heading, self.trusted etc
        This is done through indirection so we can update the defaults
        (defined  in the 'attributes' dictionary) with values from config.json
        or pages.json easily without lots of if else statements.
        If css is supplied it will overide any default css. To add additional
        style sheets on a per page basis specifiy them in pages.json.
        The same also applies with hlinks.
        css is used to set locally hosted stylesheets only. To specify
        external stylesheets use hlinks: in config.json for
        default values that will apply on all pages unless overidden, set here
        to override the default. Set in pages.json to add after default.
        """
        # set default attributes
        self.page = page.rstrip('/')
        self.defaults = get_page_attributes('defaults.json')
        self.pages = get_page_attributes('pages.json')
        title = page.lower()
        heading = page.capitalize()
        try:
            self.default_template = self.defaults['template']
        except KeyError:
            raise ConfigError('template not found in default.json')
        # will become self.name, self.title, self.heading,
        # self.footer, self.internal_css, self.trusted
        attributes = {
            'name': self.page, 'title': title,
            'navigation': top_navigation(self.page),
            'heading': heading, 'footer': None,
            'css': None, 'hlinks': None, 'internal_css': None,
            'trusted': False
        }
        # set from defaults
        attributes.update(self.defaults)
        # override with kwargs
        attributes.update(kwargs)
        # override attributes if set in pages.json
        if page in self.pages:
            attributes.update(self.pages[page])
        # set attributes (as self.name etc)  using indirection
        for attribute, value in attributes.iteritems():
            setattr(self, attribute, value)
            setattr(self, attribute, value)
        # reset these as we want to append rather than overwrite if supplied
        if 'css' in kwargs:
            self.css = kwargs['css']
        elif 'css' in self.defaults:
            self.css = self.defaults['css']
        if 'hlinks' in kwargs:
            self.hlinks = kwargs['hlinks']
        elif 'hlinks' in self.defaults:
            self.hlinks = self.defaults['hlinks']
        # append hlinks and css from pages.json rather than overwriting
        # if css or hlinks are not supplied they are set to default
        if page in self.pages:
            if 'css' in self.pages[page]:
                self.css = self.css + self.pages[page]['css']
            if 'hlinks' in self.pages[page]:
                self.hlinks = self.hlinks + self.pages[page]['hlinks']
        # append heading to default if set in config
        if app.config['default_title']:
            self.title = app.config['default_title'] + self.title

    def _get_markdown(self):
        """returns rendered markdown or 404 if source does not exist"""
        # pylint: disable=no-member
        src = self.get_page_src(self.page, 'src', 'md')
        if src is None:
            abort(404)
        else:
            return render_markdown(src, self.trusted)

    def get_page_src(self, page, directory=None, ext=None):
        """"return path of file (used to generate page) if it exists,
        or return none.
        Also returns the template used to render that page, defaults
        to static.html.
        It will optionally add an extension, to allow
        specifiying pages by route."""
        # is it stored in a config
        pagename = get_page_attribute(self.pages, page, 'src')
        if not pagename:
            pagename = page + get_extension(ext)
        if os.path.exists(src_file(pagename, directory)):
            return src_file(pagename, directory)
        else:
            return None

    def get_template(self, page):
        """returns the template for the page"""
        pagetemplate = get_page_attribute(self.pages, page, 'template')
        if not pagetemplate:
            pagetemplate = self.default_template
        if os.path.exists(src_file(pagetemplate, 'templates')):
            return pagetemplate
        else:
            raise MonomotapaError("Template: %s not found" % pagetemplate)

    def generate_page(self, contents=None):
        """return a page generator function.
        For static pages written in Markdown under src/.
        contents are automatically rendered.
        N.B. See note above in about headers"""
        if not contents:
            contents = self._get_markdown()
        template = self.get_template(self.page)
        return render_template(
            template,
            contents=Markup(contents),
            **vars(self)
        )


# helper functions

def src_file(name, directory=None):
    """return potential path to file in this app"""
    if not directory:
        return os.path.join('monomotapa', name)
    else:
        return os.path.join('monomotapa', directory, name)


def get_extension(ext):
    '''constructs extension, adding or stripping leading . as needed.
    Return null string for None'''
    if ext is None:
        return ''
    elif ext[0] == '.':
        return ext
    else:
        return '.%s' % ext


def render_markdown(srcfile, trusted=False):
    """Return markdown file rendered as html. Defaults to untrusted:
        html characters (and character entities) are escaped
        so will not be rendered. This departs from markdown spec
        which allows embedded html."""
    try:
        with open(srcfile, 'r') as f:
            src = f.read()
            src = src.decode('utf-8')
            if trusted is True:
                return markdown.markdown(src)
            else:
                return markdown.markdown(escape(src))
    except IOError:
        return None


def render_pygments(srcfile, lexer_type):
    """returns src(file) marked up with pygments"""
    if lexer_type == 'python':
        with open(srcfile, 'r') as f:
            src = f.read()
            contents = highlight(src, PythonLexer(), HtmlFormatter())
    elif lexer_type == 'html':
        with open(srcfile, 'r') as f:
            src = f.read()
            contents = highlight(src, HtmlDjangoLexer(), HtmlFormatter())
    # default to TextLexer for everything else
    else:
        with open(srcfile, 'r') as f:
            src = f.read()
            contents = highlight(src, TextLexer(), HtmlFormatter())
    return contents


def get_pygments_css(style=None):
    """returns css for pygments, use as internal_css"""
    if style is None:
        style = 'friendly'
    return HtmlFormatter(style=style).get_style_defs('.highlight')


def heading(text, level):
    """return as html heading at h[level]"""
    hltag = 'h{}'.format(str(level))
    return '\n<{}>{}</{}>\n'.format(hltag, text, hltag)


def name_obfuscator(name):
    """returns name plus a random six character string joined by a .
    characters are a..z or 0..9"""
    container = [name, '.']
    for _ in range(6):
        # 97 = asci a
        randint = random.randint(97, 132)
        # 122 = z, if over that  subtracting 75 converts it to  0..9
        if randint > 122:
            randint = randint - 75
        # add asci character
        container.append(chr(randint))
    return "".join(container)


# Define routes


@app.errorhandler(404)
def page_not_found(e):
    """ provides basic 404 page"""
    # pylint: disable=unused-argument
    defaults = get_page_attributes('defaults.json')
    try:
        css = defaults['css']
    except KeyError:
        css = None
    pages = get_page_attributes('pages.json')
    if '404' in pages:
        if'css' in pages['404']:
            css = pages['404']['css']
    return render_template(
        'static.html',
        title="404::page not found",
        heading="Page Not Found",
        navigation=top_navigation('404'),
        css=css,
        contents=Markup("This page is not there, try somewhere else.")
    ), 404


@app.route("/")
def index():
    """provides index page"""
    index = Page('index')
    return index.generate_page()


# default route is it doe not exist elsewhere
@app.route("/<path:page>")
def staticpage(page):
    """ display a static page rendered from markdown in src
    i.e. displays /page or /page/ as long as src/page.md exists.
    srcfile, title and heading may be set in the pages global
    (ordered) dictionary but are not required"""
    static_page = Page(page)
    return static_page.generate_page()


# specialized pages
@app.route("/library/<path:isbn>")
def catalogue_card(isbn):
    """Book specific pages"""
    catalogue_card = Page('catalogue_card', title=isbn, heading=isbn)
    page_src = catalogue_card.get_page_src(isbn, 'library', 'md')
    contents = render_markdown(page_src, True)
    return catalogue_card.generate_page(contents=contents)


@app.route("/source")
def source():
    """Display source files used to render a page"""
    source_page = Page(
        'source', title="view the source code",
        heading="View the Source Code",
        internal_css=get_pygments_css()
    )
    page = request.args.get('page')
    # get source for markdown if any. 404's for non-existant markdown
    # unless special page eg source
    pagesrc = source_page.get_page_src(page, 'src', 'md')
    special_pages = ['source', 'unit-tests', '404']
    if page not in special_pages and pagesrc is None:
        abort(404)
    # set enable_unit_tests  to true  in config.json to allow
    #  unit tests to be run  through the source page
    if app.config['enable_unit_tests']:
        contents = '''<p><a href="/unit-tests" class="button">Run unit tests
    </a></p>'''
        # render tests.py if needed
        if page == 'unit-tests':
            contents += heading('tests.py', 2)
            contents += render_pygments('tests.py', 'python')
    else:
        contents = ''
    # render views.py
    contents += heading('views.py', 2)
    contents += render_pygments(
        source_page.get_page_src('views.py'), 'python'
    )
    # render markdown if present
    if pagesrc:
        contents += heading(os.path.basename(pagesrc), 2)
        contents += render_pygments(pagesrc, 'markdown')
    # render jinja templates
    contents += heading('base.html', 2)
    contents += render_pygments(
        source_page.get_page_src('base.html', 'templates'), 'html'
    )
    template = source_page.get_template(page)
    contents += heading(template, 2)
    contents += render_pygments(
        source_page.get_page_src(template, 'templates'), 'html'
    )
    return source_page.generate_page(contents)


@app.route("/unit-tests")
def unit_tests():
    """display results of unit tests"""
    unittests = Page(
        'unit-tests', heading="Test Results",
        internal_css=get_pygments_css()
    )
    # exec unit tests in subprocess, capturing stderr
    capture = subprocess.Popen(
        ["python", "tests.py"],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    output = capture.communicate()
    results = output[1]
    contents = '''<p>
    <a href="/unit-tests" class="button">Run unit tests</a>
    </p><br>\n
    <div class="output" style="background-color:'''
    if 'OK' in results:
        color = "#ddffdd"
        result = "TESTS PASSED"
    else:
        color = "#ffaaaa"
        result = "TESTS FAILING"
    contents += ('''%s">\n<strong>%s</strong>\n<pre>%s</pre>\n</div>\n'''
                 % (color, result, results))
    # render test.py
    contents += heading('tests.py', 2)
    contents += render_pygments('tests.py', 'python')
    return unittests.generate_page(contents)


@app.route("/resume")
def resume():
    """resume page with footer"""
    # pylint: disable=attribute-defined-outside-init
    resume = Page('resume')
    name = name_obfuscator('paul')
    footer_content = '''<span style="font-size: 1em">
    Download this resume as a <a href="/resume.pdf">PDF</a>.<br>
    Contact me: <script>address("%s","paulmunday",2,"")</script>''' % name
    resume.footer = Markup(footer_content)
    return resume.generate_page()


@app.route("/resume.pdf")
def resume_pdf():
    """renders up to date  resume as pdf from markdown"""
    # we need to use actual files here are pdf rendering is
    # done by call to external util
    tmpfile = tempfile.NamedTemporaryFile(delete=False)
    resume = src_file('resume.md', 'src')
    output = src_file('resume.pdf', 'src')
    name = name_obfuscator('paul')
    domain = 'paulmunday.net'
    header = "#Paul Munday\nwww.{}&nbsp;&nbsp;{}@{}\n\n".format(
        domain, name, domain)
    with open(tmpfile.name, 'a') as f:
        f.write(header)
        with open(resume, 'r') as rfile:
            contents = rfile.read()
        f.write(contents)
    # render pdf and remove temp file
    subprocess.call(["pandoc", tmpfile.name, "-S", "-o", output])
    os.unlink(tmpfile.name)
    # read pdf in as string and create flask response
    with open(output, 'rb') as pdfile:
        pdf = pdfile.read()
    response = make_response(pdf, 200)
    response.headers["Content-Type"] = "Application/pdf"
    return response

read.md

## Read in 2016 (incomplete)
- <span class="h-cite"><a class="u-url p-name" href="/library/9781612194349">The Ghost Network</a>, <span class="p-author h-card"> Catie Disabato</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9781612194349">The Manhattan Projects, Vol. 1: Science Bad</a>, <span class="p-author h-card">Jonathon Hickman</span>, <span class="p-author h-card">Nick Pitarra</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9780765336798">The Fractal Prince</a>, <span class="p-author h-card">Hannu Rajaniemi</span></span>
Stats: 3 + total 2/1 male/female (0 anthology), 0 in translation, 0 poetry, 1 Graphic novels

## Read in 2015
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780307908483"> Silence Once Begun</a>, <span class="p-author h-card">Jesse Ball</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781566893541"> Faces in the Crowd</a>, <span class="p-author h-card">Valeria Luiselli</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780811217064"> The Book of Words</a>, <span class="p-author h-card">Jenny Erpenbeck</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780375709258"> Lust and Other Stories</a>, <span class="p-author h-card">Susan Minot</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780811219112"> Shantytown</a>, <span class="p-author h-card">C&eacute;sar Aira</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781447212553"> The Dark Film</a>, <span class="p-author h-card">Paul Farley</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780224077873"> What Becomes</a>, <span class="p-author h-card">A.L. Kennedy</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781564785992"> You Do Understand</a>, <span class="p-author h-card">Andrej Blatnik</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781555976897"> On Immunity: An Inoculation</a>, <span class="p-author h-card">Eula Biss</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781936365449"> The Facts of Winter</a>, <span class="p-author h-card">Paul Poissel/Paul La Farge</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781451617979"> Stone Arabia</a>, <span class="p-author h-card">Dana Spiotta</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781590176467"> Turtle Diary</a>, <span class="p-author h-card">Russell Hoban</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781903155929"> The Exiles Return</a>, <span class="p-author h-card">Elisabeth de Waal</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781555976651"> Karate Chop</a>, <span class="p-author h-card">Dorthe No</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780956254559"> The Unofficial Countryside</a>, <span class="p-author h-card">Richard Mabey</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781936873531"> A Different Bed Every Time</a>, <span class="p-author h-card">Jac Jemc</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781843911227"> The Secret Lives of Wives</a>, <span class="p-author h-card">Pietro Aretino</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781609805517"> Natural Histories</a>, <span class="p-author h-card">Guadalupe Nettel</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780972869263"> Dreams and Stones</a>, <span class="p-author h-card">Magaret Tulli</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780374286644"> Ways of Going Home</a>, <span class="p-author h-card">Alejandro Zambra</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781854114617"> Kathryn Simmonds</a>, <span class="p-author h-card">Sunday at the Skin Launderette</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780811224215"> The Strange Case of Rachel K</a>, <span class="p-author h-card">Rachel Kushner</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780571315789"> Train Songs: Poetry of the Railway</a>, (ed. Don Patterson)</span></span>
  -  <span class=h-cite><a class="u-url p-name" href="/library/9781935635147">Baltics</a>, <span class="p-author h-card">Tomas Transtr&ouml;mer</span>. Quote: <a href="/baltics">_&ldquo;My grandmother's story before its forgotten&rdquo;_</a>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781620405659"> Things I Don&apos;t Want To Know</a>, <span class="p-author h-card">Deborah Levy</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781590171950"> Ice</a>, <span class="p-author h-card">Vladimir Sorokin</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780571284573"> The World&apos;s Two Smallest Humans</a>, <span class="p-author h-card">Julia Copus</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780720612684">Ice</a>, <span class="p-author h-card">Anna Kavan</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781569715864">Lone Wolf & Cub, Vol 14: Day of the Demons</a>, <span class="p-author h-card"> Kazuo Koike &amp; Goseki Kojima</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781596434219">The Wrenchies</a>, <span class="p-author h-card">Farel Dalrymple</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781627792103">The Assassination of Margaret Thatcher</a>, <span class="p-author h-card">Hilary Mantel</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781891830709">The Ticking</a>, <span class="p-author h-card">Rene&eacute; French</span></span>
  - <span class="h-cite"><span class="u-url p-name">Students for a Democratic Society: A Graphic History</span>, <span class="p-author h-card">Harvey Pekar</span>, <span class="p-author h-card">Gary Dumm</span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9781566893565"> Sidewalks</a>, <span class="p-author h-card">Valeria Luiselli</span></span> Quote:<em><a href="/sidewalks">&ldquo;Nowadays, only someone sensible enough to own a bicycle can claim to possess an extravagantly free spirit...&rdquo;</a></em></span>
  - <span class="h-cite"><a class="u-url p-name" href="library/9780224027298">Swallowing Geography</a>, <span class="p-author h-card">Deborah Levy</span></span> Quote:<em><a href="/citizen">&ldquo;What does it mean to be named a citizen...&rdquo;</a></em></span>
  - <span class="h-cite"><a class="u-url p-name" href="library/9780914232889">The Heat Death of the Universe</a>, <span class="p-author h-card">Pamela Zoline</span></span></span>
  - <span class="h-cite"><a class="u-url p-name" href="library/9781555974473">Landing Light:Poems</a>, <span class="p-author h-card">Don Paterson</span></span></span>
  - <span class="h-cite"><a class="u-url p-name" href="/library/9780345806871"> Dept. of Speculation</a>, <span class="p-author h-card">Jenny Offill</span></span> Quote:<em><a href="/sleep_like_a_baby">&ldquo;And that phrase&mdash;&ldquo;sleeping like a baby.&rdquo;&rdquo;</a></em></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9781590173060">Short Letter, Long Farewell</a>, <span class="p-author h-card">Peter Handke</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9781607067375">Comeback</a>, <span class="p-author h-card">Ed Brisson</span>, <span class="p-author h-card">Michael Walsh</span>, <span class="p-author h-card">Jordie Bellaire</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9780889740464">Sing Me No More</a>, <span class="p-author h-card">Lynnette Dueck</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9781847771186">Then</a>, <span class="p-author h-card">Alison Brackenbury</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9781937658069">The Obscene Madam D</a>, <span class="p-author h-card">Hilda Hilst</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9780988814974">Palefire</a>, <span class="p-author h-card">Mk  Reed</span>, <span class="p-author h-card">Farel Dalrymple</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9780375406508">Here</a>, <span class="p-author h-card">Richard McGuire</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9781606998373">D&ouml;rfler</a>, <span class="p-author h-card">Jeremy Baum</span></span>
- <span class="h-cite"><span class=p-name" href="/library/9780861662302"> Nemo, Roses of Berlin</span>, <span class="p-author h-card">Alan Moore</span>,<span class="p-author h-card">Kevin 0'Neill</span></span> (library).
- <span class="h-cite"><a class="u-url p-name" href="/library/9780989760751">Vertigo</a>, <span class="p-author h-card">Joanna Walsh</span></span>
- <span class="h-cite"><a class="u-url p-name" href="/library/9780765375889">The  Quantum Thief</a>, <span class="p-author h-card">Hannu Rajaniemi</span></span>


Stats: 49 Total, 22/26 male/female (1 anthology), 17 in translation, 7 poetry, 9 Graphic novels.


2014, 2013: missing

## Read in 2012

  -  Your Face Tomorrow: Fever and Spear, Javier Marias
  -  The Particular Sadness of Lemon Cake, Aimee Bender
  -  Metropole, Ferenc Karinthy
  -  The Sixties, Jenifer Diski
  -  The Arrival, Shaun Tan
  -  I, City,  Pavel Brycz
  -  My Revolutions, Hari Kunzu
  -  The Summer Book, Tove Jansson
  -  Lightning Rods, Helen DeWitt
  -  C&eacute;sar Aira, Ghosts
  -  The Night Bookmobile, Audrey Niffenegger
  -  Ten Tales Tall & True, Alasdair Gray.
  -  Asterios Polyps, David Mazzucchelli
  -  The Museum of Unconditional Surrender, Dubravka Ugresic
  -  Florida, Christine Schutt
  -  Bonsai, Alejandro Zambra
  -  Netherland, Joseph O'Neill
  -  Beautiful Mutants, Deborah Levy
  -  Selected Stories, Robert Walser
  -  Flame Alphabet, Ben Marcus
  -  The Bug, Ellen Ullman
  -  Reticence, Jean-Philippe Toussaint
  -  Men in Space, Tom McCarthy
  -  Everyday Life, Lydie Salvayre
  -  The Heat of the Day, Elizabeth Bowen
  -  Everything and Nothing, Jorge Luis Borges
  -  Vicky Swanky is a Beauty, Diane Williams
  -  The Museum of Useless Efforts, Cristina Peri Rossi
  -  What We Were Doing and Where We Were Going, Damion Searls
  -  Vacant Possession, Hilary Mantel.
  -  No One, Gwena&euml;lle Aubry
  -  Too Loud a Solitude, Bohumil Hrabal

  Stats: 32 total, 17/15 male/female, 14 in translation, 0 poetry, 3 graphic novels.

  2007-2011: missing

## Read in 2006
  -  The Daedalus Book of Absinthe, Phil Baker
  -  Hardboiled/Hard Luck, Banana Yoshimoto
  -  A Smell of Fish, Matthew Sweeny
  -  Abbducted: How people came to believe they were kidnapped by aliens, Susan Clancy
  -  Beasts of No Nation, Uzodinma Iweala
  -  Snow, Orhan Pamuk
  -  Wrong About Japan, Peter Carey
  -  Bleak House, Charles Dickens
  -  Honored Guest, Joy Williams
  -  The Peoples Act of Love, James Meek
  -  Facts About the Moon, Dorianne Laux
  -  Cutty, One Rock, August Kleinzahler
  -  Hateship, Friendship, Courtship, Loveship, Marriage, Alice Munro
  -  The State of the Prisons, Sinead Morrisey
  -  Potential Stranger, Killarney Clary
  -  I Could Ride All Day in My Cool Blue Train, Peter Hobbs
  -  Lone Wolf and Cub 1: The Assassin's Road, Kazuo Koike & Goseki Kojima
  -  Lone Wolf and Cub 2: The Gateless Barrier,Kazuo Koike & Goseki Kojima
  -  Cloud Atlas, David Mitchell
  -  Lone Wolf and Cub 3:The Flute of the Fallen Tiger, Kazuo Koike & Goseki Kojima
  -  Quiver, Deren Rees-Jones
  -  Lone Wolf and Cub 4: The Bell Warden, Kazuo Koike & Goseki Kojima
  -  The Good Neighbour, John Burnside
  -  Take Me With You, Polly Clark
  -  Thank You for Not Reading, Dubravka Ugresic
  -  Controlled Burn, Scott Wolvern
  -  Lone Wolf and Cub 5: Black Wind, Kazuo Koike & Goseki Kojima
  -  Lolita, Victor Nabokov
  -  We Were Pedestrians, Gerald Woodward
  -  Lone Wolf and Cub 6: Lanterns for the Dead, Kazuo Koike & Goseki Kojima
  -  My Brother is Getting Arrested Again, Daisy Fried
  -  Quicksand Beach, Kate Bingham

Stats: 32 total 20/12 male/female, 9 in translation, 11 poetry, 6 graphic novels.

## Read in 2005
  -  The Light Trap, John Burnside
  -  Civilisation & Its Discontents, Sigmund Freud
  -  The Full Indian Rope Trick, Colette Bryce
  -  The Time of Our Singing, Richard Powers
  -  The Girl in the Flammable Skirt, Aimee Bender
  -  Breaking News, Ciaran Carson
  -  Pillow Talk in Europe and Other Places, Deborah Levy
  -  Written on the Body, Jeannette Winterson
  -  The Whole Story and other stories, Ali Smith
  -  Epileptic, David B.
  -  In Defence of Adultery, Julia Copus
  -  Pattern Recognition, William Gibson
  -  Field Study, Rachel Seiffert
  -  Blood and Soap, Linh Dinh
  -  Roberts Corpus, Michael Symons
  -  Assorted Fire Events, David Means
  -  The Book of Orgasms, Nin Andrews
  -  In Praise of Shadows, Junchiro Tanizaki
  -  Lighthousekeeping, Jeanette Winterson
  -  The Creative Habit, Twyla Tharp
  -  Little Black Book of Stories, A.S. Byatt
  -  The School of Whoredom,  Pietro Aretino
  -  These Days, Leontia Flynn
  -  Stolen Love Behaviour, John Stammers
  -  Schooling, Heather McGowan
  -  Slow Air, Robin Robertson
  -  A Heart So White, Javier Mar&iacute;as
  -  Mirror, Window, Jessica Abel
  -  Minsk, Lavina Greenlaw
  -  We Need To Talk About Kevin, Lionel Shriver
  -  Groundwater, Matthew Hollis
  -  Asleep, Banana Yoshimoto
  -  Intimates, Helen Farish
  -  Paradise, A.L. Kennedy
  -  After the Plague, T.C. Boyle
  -  Monogamy, Adam Phillips
  -  In Doctor No's Garden, Henry Shukman
  -  The Origins of the Final Solution, Christopher Browning
  -  The Perfect Egg, Aldo Buzi
  -  Little, David Truer
  -  The Point of Splitting, Sally Read
  -  Black Hole, Charles Burns
  -  Wasting Game, Phillip Gross
  -  Other Electricities, Ander Monson
  -  The People's Act of Love, James Meek
  -  The Accidental, Ali Smith
  -  Legion, David Harsent
  -  Rent Girl, Michelle Tea
  -  Beyond Black, Hilary Mantel
  -  My Life in CIA, Harry Matthews
  -  The Strange Hours Travelers Keep, August Kleinzahler
  -  Berlin: City of Stones, Jason Lutes

  Stats: 52 total, 29/23 male/female, 7 in translation, 14 poetry, 5 graphic novels.

base.html

<!DOCTYPE html>
<!--

Monomotapa - A Micro CMS
Copyright (C) 2014, Paul Munday.

PO Box 28228, Portland, OR, USA 97228
paul at paulmunday.net

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero  Public License as published by
the Free Software Foundation, either version 3 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 Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

There should also be a copy of the AGPL in src/license.md that should be
accessible by going to <a href ="/license">/license<a> on this site.
-->
<html>
<head>
<title>{% if title -%}{{title}}{% endif %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{%- if css -%}
{%- for file in css %}
    <link href="{{ url_for('static', filename=file) }}" rel="stylesheet" type="text/css"  />
    {%- endfor -%}
{%- endif %}
{% if internal_css %}
<style type="text/css">
    {{internal_css}}
</style>
{% endif %}
{%- if hlinks -%}
{%- for item in hlinks -%}
<link 
        {%- if item.href %} href="{{item.href}}"{% endif -%}
        {%- if item.rel %} rel="{{item.rel}}"{% endif -%} 
        {%- if item.type %} type="{{item.type}}"{% endif -%}
        {%- if item.media %} type="{{item.media}}"{% endif -%}
        {%- if item.hreflang %} type="{{item.hreflang}}"{% endif -%}
        {%- if item.charset %} type="{{item.charset}}"{% endif -%}
        >
{% endfor %}
{%- endif -%}
</head>
<body>
<div id="wrap">
    <div id="main">
        <div id="nav">
           <ul id="nav">
            {%- for item in navigation.navigation.itervalues() -%}
                <li><a href="
                {%- if item.url -%}{{item.url}}
                {%- elif item.urlfor -%}
                    {%- if item.urlfor == "source" -%}
                        {{ url_for(item.urlfor, page=navigation.page) }}
                    {%- else -%}
                        {{ url_for(item.urlfor) }}
                    {%- endif -%}
                {%- else -%}
                    {{ url_for('staticpage', page=item.base) }}
                {%- endif -%}
                
                {%- if item.rel -%}
                         " rel="{{item.rel}} 
                    {%- endif -%}
                ">{{item.link_text}}</a></li>
                {% endfor -%}
            </ul>
        </div>
        {% block content %}{% endblock %}
           </div>
</div>
<div id="footer">
    <p id="footer">
    <!-- footer goes here -->
    {% if footer %}
        {{footer}}
    {% endif %}

    </p>
</div>
</body>
</html>

static.html

{% extends "base.html" %}
    {% block content %}
    <div> 
            {% if heading %}<h1>{{heading}}</h1>{% endif %}
        </div> 
        <div id="messages">
            <hr>
            {% with messages = get_flashed_messages() %}
                {% if messages %}
                    {% for message in messages %}
                        {{ message }}
                    {% endfor %}
                {% endif %}
            {% endwith %}
        </div>
        <div id="content">
        {{contents}}
        </div>
    {% endblock %}