+++++++++++++ Blog Tutorial +++++++++++++ :author: Ian Bicking :revision: $Rev: 2321 $ :date: $LastChangedDate: 2005-04-09 17:55:39 -0500 (Sat, 09 Apr 2005) $ .. contents:: Introduction ============ This tutorial will go through the process of creating a blog using `Python Paste `_, 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/wkblog' >>> import sys >>> clear_dir(BASE) >>> run("app-setup create --template=webkit_zpt %s" % BASE) >>> os.chdir(BASE) :: $ export PYTHONPATH=/path/to/Paste:$PYTHONPATH $ BASE=/var/www/wkblog $ app-setup 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 * try: from datetime import datetime now = datetime.now except ImportError: from mx import DateTime now = DateTime.now 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=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() password_encoded = StringCol() role = StringCol(notNull=True, default='user') """) .. raw:: html :file: resources/BlogTutorial/db.py.v1.gen.html A few things to note: * Sadly, both datetime_ and mxDateTime_ are pseudo-standards. Well, datetime is the standard, but mxDateTime has the legacy. So we have to do this little dance to import one or the other. (We use ``now`` later as a default) .. _datetime: http://python.org/doc/current/lib/module-datetime.html .. _mxDateTime: http://www.egenix.com/files/python/mxDateTime.html * 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:: 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('sqlobject-admin create -f server.conf -m wkblog.db')