+++++++++++++
Blog Tutorial
+++++++++++++

:author: Ian Bicking <ianb@colorstudy.com>
:revision: $Rev$
:date: $LastChangedDate$

.. contents::

.. note::

    This tutorial is not yet finished.  What you see is what you get,
    and yeah that's not a whole lot.

Introduction
============

This tutorial will go through the process of creating a blog using
`Python Paste <http://pythonpaste.org>`_, SQLObject_, and `Zope Page
Templates`_.  This blog will rely heavily on static publishing -- that
is, when at all possible flat HTML pages will be written to disk.  For
some parts (e.g., posting a new item) this will of course be
infeasible, but for most of the site this should work fine.

.. _SQLObject: http://sqlobject.org
.. _Zope Page Templates: http://www.zope.org/DevHome/Wikis/DevSite/Projects/ZPT/FrontPage

As much as possible, this code will be accompanied by unit tests, and
test-driven methodologies.  Doing test-driven documenting of the
incremental process of creating test-driven software may get a little
hairy, but wish me luck!

This tutorial presupposes you are somewhat comfortable with the basic
stack -- the `To-Do Tutorial`_ is a better place to start for a
beginner.

.. _To-Do Tutorial: TodoTutorial.html

Setting Up The App
==================

.. note::

    We're doing all this in the Python interpreter, even though you'd normally do
    some of this in the shell.  This way the authors of this tutorial
    can use something called doctest_, which allows this tutorial to
    be tested Python in an automated way.

    .. _doctest: http://python.org/doc/current/lib/module-doctest.html

.. comment:

    >>> from paste.tests.doctest_webapp import *
    >>> BASE = '/var/www/example-builds/wwblog'
    >>> import sys
    >>> clear_dir(BASE)
    >>> run("paster create --template=webkit_zpt %s" % BASE)
    >>> os.chdir(BASE)

::

    $ export PYTHONPATH=/path/to/Paste:$PYTHONPATH
    $ BASE=/var/www/example-builds/wwblog
    $ paster create --template=webkit_zpt $BASE
    $ cd $BASE

The Model
=========

Since we're using SQLObject, we'll be doing the complete model in
that.  The predecessor of this blog used flat files, custom-written
indexes, and simple rfc822_ based files for structure.  It did not
scale well at all.

.. _rfc822: http://python.org/doc/current/lib/module-rfc822.html

Here's the model:

.. run:

    create_file('db.py', 'v1', r"""
    from sqlobject import *
    
    class Article(SQLObject):
        url = StringCol(notNull=True)
        title = StringCol()
        content = StringCol(notNull=True)
        content_mime_type = StringCol(notNull=True)
        author = ForeignKey('User')
        parent = ForeignKey('Article', default=None)
	created = DateTimeCol(notNull=True, default=DateTimeCol.now)
        last_updated = DateTimeCol(default=None)
        atom_id = StringCol()
        hidden = BoolCol(notNull=True, default=False)
        article_type = StringCol(notNull=True, default='article')
        categories = RelatedJoin('Category')
    
    class Category(SQLObject):
        name = StringCol(alternateID=True)
        articles = RelatedJoin('Article')
    
    class User(SQLObject):
        class sqlmeta:
            table = 'user_info'
        username = StringCol(alternateID=True)
        email = StringCol()
        name = StringCol()
        homepage = StringCol()
        password_encoded = StringCol()
        role = StringCol(notNull=True, default='user')
    """)

.. raw:: html
   :file: resources/blog-tutorial/db.py.v1.gen.html

A few things to note:

* All the columns allow ``NULL`` by default, unless we say
  ``notNull=True``.

* ``ForeignKey('User')`` is a join to another table (the ``User``
  table, of course).  We have to use strings to refer to other class,
  because in this case the ``User`` class hasn't even been created.
  Generally all references between classes are by name.

* ``created`` has a default.  You can give a fixed default (like
  ``True`` or ``3``), or you can pass in a function that is called.
  In this case, if you don't indicate ``Article(...,
  created=something)`` then ``created`` will be the current date and
  time.  Unless a default is explicitly given, it is an error to leave
  a column out of the constructor.  ``NULL`` (which is ``None`` in
  Python) is *not* considered a default.

* Some column types don't relate directly to database types.  For
  instance, though PostgreSQL has a ``BOOLEAN`` type, most databases
  don't, so ``BoolCol`` translates to some kind of ``INT`` column on
  those database.

* ``RelatedJoin('Category')`` creates a mapping table
  (``article_category``) and is a many-to-many join between articles
  and categories.

* ``user`` isn't a valid table name in many databases, so while the
  class is named ``User``, the table actually is ``user_info``.  This
  kind of extra information about a class is typically passed in
  through the ``sqlmeta`` inner class.

These classes have lots of other *behavior*, but this should be a good
list of actual information.  We'll add more behavior later.

Now we'll create the database.  First we configure it, adding these
lines to ``server.conf``::

    import os
    database = 'sqlite:%s/data.db' % os.path.dirname(__file__)

You could also use::

    database = 'mysql://user:passwd@localhost/dbname'
    database = 'postgresql://user:password@localhost/dbname'

.. comment (change server.conf)

    >>> change_file('server.conf', [('insert', 2, r"""import os
    ... database = 'sqlite:%s/data.db' % os.path.dirname(__file__)
    ...
    ... """)])

Now we'll use ``sqlobject-admin`` to set up the tables:

.. comment (do it)

    >>> run_command('sqlobject-admin create -f server.conf '
    ...             '-m wwblog.db', 'create', and_print=True)


.. raw:: html
   :file: resources/blog-tutorial/shell-command.create.gen.html

Fixture Data
------------

To test things later we'll need a bit of data to make the tests
interesting.  It's best if we write code to clear any data and put
known data in -- that way we can restore the database at any time to a
known state, and can write our tests against that data.

We'll add some code to the end of ``db.py``:

.. comment (change)

    >>> append_to_file('db.py', 'append-fixture', r"""
    ... 
    ... from paste import CONFIG
    ... def reset_data():
    ...     sqlhub.processConnection = connectionForURI(CONFIG['database'])
    ...     for soClass in (User, Category, Article):
    ...         soClass.clearTable()
    ...     auth = User(username='author', email='author@example.com',
    ...                 name='Author Person', password_encoded=None,
    ...                 role='author', homepage=None)
    ...     user = User(username='commentor', email='comment@example.com',
    ...                 name='Comment Person', password_encoded=None,
    ...                 role='user', homepage='http://yahoo.com')
    ...     programming = Category(name='Programming')
    ...     family = Category(name='family')
    ...     a1 = Article(url='/2004/05/01/article1.html',
    ...                  title='First article',
    ...                  content='This is an article',
    ...                  content_mime_type='text/html',
    ...                  author=auth, parent=None,
    ...                  last_updated=None, atom_id=None)
    ...     a2 = Article(url='/2004/05/10/article2.html',
    ...                  title='Second article',
    ...                  content='Another\narticle',
    ...                  content_mime_type='text/plain',
    ...                  author=auth, parent=None,
    ...                  last_updated=None, atom_id=None)
    ...     c1 = Article(url='/2004/05/01/article1-comment1.html',
    ...                  title=None, content='Nice article!',
    ...                  content_mime_type='text/x-untrusted-html',
    ...                  author=user, parent=a1, last_updated=None,
    ...                  atom_id=None, article_type='comment')
    ...     a1.addCategory(programming)
    ...     a1.addCategory(family)
    ... """)

.. raw:: html
   :file: resources/blog-tutorial/db.py.append-fixture.gen.html

Test Fixture
------------

See `Testing Applications With Paste <testing-applications.html>`_ for
more on the details of how we set up testing.  We'll be using `py.test
<http://codespeak.net/py/current/doc/test.html>`_ for the testing
framework.

First, lets create our own test fixture.  We'll create a directory
``tests/`` and add a file ``fixture.py``.

.. comment (create files):

    >>> create_file('tests/__init__.py', 'v1', '#\n')
    >>> create_file('tests/fixture.py', 'v1', r"""
    ... from paste.tests.fixture import setup_module as paste_setup
    ... from wwblog import db
    ...
    ... def setup_module(module):
    ...     paste_setup(module)
    ...     db.reset_data()
    ... """)

.. raw:: html
   :file: resources/blog-tutorial/tests/fixture.py.v1.gen.html

Now in each test we'll do::

    from wwblog.tests.fixture import setup_module

And that will give us a consistent state for the module (note that
data isn't reset between each test in the module, just once for the
module, so we'll have to be aware of that).

Let's write a first test:

.. comment (create test):

    >>> create_file('tests/test_db.py', 'v1', r"""
    ... from fixture import setup_module
    ... from wwblog.db import *
    ...
    ... def test_data():
    ...     # make sure we have the two users we set up
    ...     assert len(list(User.select())) == 2
    ...     # and get the first article for testing
    ...     a1 = list(Article.selectBy(title='First article'))[0]
    ...     # make sure it has categories, then make sure the
    ...     # categories contain this article as well
    ...     assert len(list(a1.categories)) == 2
    ...     for cat in a1.categories:
    ...         assert a1 in cat.articles
    ... """)

.. raw:: html
   :file: resources/blog-tutorial/tests/test_db.py.v1.gen.html

For the most part, this stuff is already tested by SQLObject, but this
is a basic sanity check, and a test that we have set up the classes
properly.  One problem, though, is that we have to make sure that
``sys.path`` is set up properly.  We could set ``$PYTHONPATH``, but
that can be a bit annoying; we'll put it in a special file
``conftest.py`` that py.test loads up:

.. comment (create):

    >>> create_file('conftest.py', 'v1', r"""
    ... import sys, os
    ... sys.path.append('/path/to/Paste')
    ... sys.path.append(os.path.dirname(os.path.dirname(__file__)))
    ... from paste.util.thirdparty import add_package
    ... add_package('sqlobject')
    ... """)

.. raw:: html
   :file: resources/blog-tutorial/conftest.py.v1.gen.html
    
Now we should be able to run py.test:

.. comment (do):

    >>> run_command('py.test', 't1', and_print=True)
    inserting into sys.path: ...
    =...= test process starts =...=
    testing-mode: inprocess
    executable:   .../python  (...)
    using py lib: .../py <rev ...>
    .../test_db.py[1] .
    =...= tests finished: 1 passed in ... seconds =...=

.. raw:: html
   :file: resources/blog-tutorial/shell-command.t1.gen.html

Very good!  Alright then, on to making an application...

Static Publishing
-----------------

Remember I said something about static publishing?  So... what does
that mean?

Well, it means that when possible we should write files out to disk.
These files might be in their final form, though in some environments
it might be nice to write out files that are interpreted by `server
side includes <http://httpd.apache.org/docs/mod/mod_include.html>`_ or
PHP.  

By generating what is effectively code, the "static" files can contain
dynamic portions, e.g., a running list of recently-updated external
blogs.  But more importantly, many changes can be made without
generating the entire site; changes to the look of the site, of
course, but also smaller things, like up-to-date archive links on the
sides of pages and other little bits of code.

This tutorial will work with server-side includes, because they are
dumb enough that we won't be tempted to push too much functionality
into them (and we might be able to extract their functionality into
fully-formed pages); but they'll also save us a lot of work early on.
If you haven't used server-side includes you can read the document I
linked too, but you can also probably pick it up easily enough from
the examples.

URL Layout
----------

The blog "application" will be available through some URL (which we
can figure out at runtime).  But everything else gets written onto
disk with some URL equivalent, so that path and URL will have to be
configurable.

The Front Page
--------------

First we'll set up a simple front page.  We'll write a new
``index.py``:

.. comment (do so):

    >>> create_file('web/index.py', 'v1', r"""
    ... """)

.. raw:: html
   :file: resources/blog-tutorial/web/index.py.v1.gen.html
