summaryrefslogtreecommitdiff
path: root/docs/blog-tutorial.txt
blob: d0931e9c0965ca0e1c890616447f68c9db2b7cf3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
+++++++++++++
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