How does TurboGears2 help you get development done quickly? We’ll show you by developing a simple wiki application that should take you no more than 20 minutes to complete. We’re going to do this without explaining the steps in detail (that is what this book is for, after all). As a result, you’ll see how easily you can make your own web applications once you are up to speed on what TurboGears2 offers.
If you’re not familiar with the concept of a wiki you might want to check out the Wikipedia entry. Basically, a wiki is an easily-editable collaborative web content system that makes it trivial to link to pages and create new pages. Like other wiki systems, we are going to use CamelCase words to designate links to pages.
If you have trouble with this tutorial ask for help on the TurboGears discussion list, or on the IRC channel #turbogears. We’re a friendly bunch and, depending what time of day you post, you’ll get your answer in a few minutes to a few hours. If you search the mailing list or the web in general you’ll probably get your answer even faster. Please don’t post your problem reports as comments on this or any of the following pages of the tutorial. Comments are for suggestions for improvement of the docs, not for seeking support.
If you want to see the final version you can download a copy of the wiki code.
To go through this tutorial, you’ll need:
Python 2.4, 2.5, 2.6 or 2.7. Note that Mac OSX 10.5 (Leopard) comes with Python 2.5 pre-installed.
virtualenv. Not that on many Linux based systems you will need to install a Python development package for this to work, such as python-devel or python-dev. In addition, virtualenv is available via most package managers, so can be installed that way.
A web browser.
Your favorite editor.
Two command line windows (you only need one, but two is nicer).
Optional: If you’re not aware of it, you may also find the ipython shell to be helpful. It supports attribute tab completion for many objects (which can help you find the method you’re searching for) and can display contextual help if you append a question mark onto the end of an object or method. You can do the same in the standard shell with the dir() and help() functions, but ipython is more convenient. ipython has a number of other convenient features, like dropping into the debugger on an error; take a look at the ipython docs for more information. You can install it with:
$ easy_install ipython
This tutorial doesn’t cover Python at all. Check the Python Documentation page for more coverage of Python.
The use of a virtual environment is highly recommended. It allows you to segregate your development work from the system, so you can freely experiment without worrying about breaking some other package. The use of it is two steps: Making the directory structure for the virtual environment, and activating it.
This is the same across all platforms. Use the following command:
$ virtualenv --no-site-packages path/to/virtualenvironment
The “–no-site-packages” ensures that your virtual environment is just what comes with Python. Using that, you ensure that you have no unknown conflicts while doing your development.
On Linux and other UNIX (or UNIX-like) operating systems, use this command:
$ source path/to/virtualenvironment/bin/activate
On Windows, use this command:
C:\> path\to\virtualenvironment\bin\activate
On all platforms, when you are done, use the “deactivate” command to return to using your system wide Python installation.
Once you have your development environment prepared (using the instructions for making a virtualenv above), installing TurboGears2 itself is extremely easy. Run this command:
$ easy_install -i http://tg.gy/current/index/ tg.devtools
Wait a few moments as the dependencies are installed and prepared for you.
TurboGears2 provides a suite of tools for working with projects by adding several commands to the Python command line tool paster. A few will be touched upon in this tutorial. (Check the TurboGears2 Command Line Reference for a full listing.) The first tool you’ll need is quickstart, which initializes a TurboGears project. Go to a command line window and run the following command:
$ paster quickstart
You’ll be prompted for the name of the project (this is the pretty name that human beings would appreciate), and the name of the package (this is the less-pretty name that Python will like). Here’s what our choices for this tutorial look like:
$ paster quickstart
Enter project name: Wiki 20
Enter package name [wiki20]:
Would you prefer mako templates? (yes/[no]): no
Do you need authentication and authorization in this project? ([yes]/no): yes
We recommend you use the names given here: this documentation looks for files in directories based on these names.
Now paster will spit out a bunch of stuff:
Selected and implied templates:
tg.devtools#turbogears2 TurboGears 2. Standard Quickstart Template
...etc...
reading manifest file 'Wiki_20.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'Wiki_20.egg-info/SOURCES.txt'
This creates a few files in a directory tree just below your current directory. You will notice that the quickstart created a directory without spaces for convenience: project name “Wiki 20” resulted in the directory name “Wiki-20”. Go in there and take a look around:
$ cd Wiki-20
You need to update the dependencies in the file “setup.py”. Currently, it looks like this:
install_requires=[
"TurboGears2 >= 2.1.1",
"Genshi",
"zope.sqlalchemy >= 0.4",
"repoze.tm2 >= 1.0a5",
"sqlalchemy",
"sqlalchemy-migrate",
"repoze.what-quickstart",
"repoze.what >= 1.0.8",
"repoze.what-quickstart",
"repoze.who-friendlyform >= 1.0.4",
"repoze.what-pylons >= 1.0",
"repoze.what.plugins.sql",
"repoze.who==1.0.19",
"tgext.admin >= 0.3.9",
"tw.forms",
],
You need to add “docutils” to the list. TurboGears2 does not require docutils, but the wiki we are building does. In addition, if you are using Python 2.4, you need to add “pysqlite”. For this tutorial, we are using the SQLite database, and Python 2.4 does not include support for it out of the box.
Now to be able to run the project you will need to install it and its dependencies. This can be quickly achieved by running from inside the Wiki-20 directory:
$ python setup.py develop
Then paster provides a simple mechanism for running a TurboGears project. Again from the Wiki-20 directory, run this command:
$ paster serve --reload development.ini
The --reload flag means that changes that you make in the project will automatically cause the server to restart itself. This way you immediately see the results.
Point your browser to http://localhost:8080 , and you’ll see a nice welcome page. You now have a working project! And you can access the project from within the python/ipython shell by typing:
$ paster shell development.ini
If ipython is installed within your virtual environment, it will be the default shell. Right now, we’re not going to do much with the shell, but you may find other tutorials which use it to add data to the database.
If you take a look at the code that quickstart created, you’ll see everything necessary to get up and running. Here, we’ll look at the two files directly involved in displaying this welcome page.
TurboGears follows the Model-View-Controller paradigm (a.k.a. “MVC”), as do most modern web frameworks like Rails, Django, Struts, etc.
To minimize duplication of effort web frameworks use templating engines which allow you to create “template” files. These specify how a page will always look, with hooks where the templating engine can substitute information provided by your web application. TurboGears 2’s default templating engine is Genshi, although several other engines are supported out of the box and can be configured in your config/app_cfg.py file (see part IV of this book).
Todo
add link to part IV when it is written
Wiki-20/wiki20/controllers/root.py (see below) is the code that causes the welcome page to be produced. After the imports the first line of code creates our main controller class by inheriting from TurboGears’ BaseController:
class RootController(BaseController):
The TurboGears 2 controller is a simple object publishing system; you write controller methods and @expose() them to the web. In our case, there’s a single controller method called index. As you might guess, this name is not accidental; this becomes the default page you’ll get if you go to this URL without specifying a particular destination, just like you’ll end up at index.html on an ordinary web server if you don’t give a specific file name. You’ll also go to this page if you explicitly name it, with http://localhost:8080/index. We’ll see other controller methods later in the tutorial so this naming system will become clear.
The @expose() decorator tells TurboGears which template to use to render the page. Our @expose() specifies:
@expose('wiki20.templates.index')
This gives TurboGears the template to use, including the path information (the .html extension is implied). We’ll look at this file shortly.
Each controller method returns a dictionary, as you can see at the end of the index method. TG takes the key:value pairs in this dictionary and turns them into local variables that can be used in the template.
# -*- coding: utf-8 -*-
"""Main Controller"""
from tg import expose, flash, require, url, request, redirect
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from tgext.admin.tgadminconfig import TGAdminConfig
from tgext.admin.controller import AdminController
from repoze.what import predicates
from wiki20.lib.base import BaseController
from wiki20.model import DBSession, metadata
from wiki20 import model
from wiki20.controllers.secure import SecureController
from wiki20.controllers.error import ErrorController
__all__ = ['RootController']
class RootController(BaseController):
"""
The root controller for the Wiki-20 application.
All the other controllers and WSGI applications should be mounted on this
controller. For example::
panel = ControlPanelController()
another_app = AnotherWSGIApplication()
Keep in mind that WSGI applications shouldn't be mounted directly: They
must be wrapped around with :class:`tg.controllers.WSGIAppController`.
"""
secc = SecureController()
admin = AdminController(model, DBSession, config_type=TGAdminConfig)
error = ErrorController()
@expose('wiki20.templates.index')
def index(self):
"""Handle the front-page."""
return dict(page='index')
@expose('wiki20.templates.about')
def about(self):
"""Handle the 'about' page."""
return dict(page='about')
@expose('wiki20.templates.environ')
def environ(self):
"""This method showcases TG's access to the wsgi environment."""
return dict(environment=request.environ)
@expose('wiki20.templates.data')
@expose('json')
def data(self, **kw):
"""This method showcases how you can use the same controller for a data page and a display page"""
return dict(params=kw)
@expose('wiki20.templates.authentication')
def auth(self):
"""Display some information about auth* on this application."""
return dict(page='auth')
@expose('wiki20.templates.index')
@require(predicates.has_permission('manage', msg=l_('Only for managers')))
def manage_permission_only(self, **kw):
"""Illustrate how a page for managers only works."""
return dict(page='managers stuff')
@expose('wiki20.templates.index')
@require(predicates.is_user('editor', msg=l_('Only for the editor')))
def editor_user_only(self, **kw):
"""Illustrate how a page exclusive for the editor works."""
return dict(page='editor stuff')
@expose('wiki20.templates.login')
def login(self, came_from=url('/')):
"""Start the user login."""
login_counter = request.environ['repoze.who.logins']
if login_counter > 0:
flash(_('Wrong credentials'), 'warning')
return dict(page='login', login_counter=str(login_counter),
came_from=came_from)
@expose()
def post_login(self, came_from='/'):
"""
Redirect the user to the initially requested page on successful
authentication or redirect her back to the login page if login failed.
"""
if not request.identity:
login_counter = request.environ['repoze.who.logins'] + 1
redirect('/login',
params=dict(came_from=came_from, __logins=login_counter))
userid = request.identity['repoze.who.userid']
flash(_('Welcome back, %s!') % userid)
redirect(came_from)
@expose()
def post_logout(self, came_from=url('/')):
"""
Redirect the user to the initially requested page on logout and say
goodbye as well.
"""
flash(_('We hope to see you soon!'))
redirect(came_from)
Wiki-20/wiki20/templates/index.html (see below) is the template specified by the @expose() decorator, so it formats what you view on the welcome screen. Look at the file; you’ll see that it’s standard XHTML with some simple namespaced attributes. This makes it very designer-friendly, and well-behaved design tools will respect all the Genshi attributes and tags. You can even open it directly in your browser.
Genshi directives are elements and/or attributes in the template that are usually prefixed with py:. They can affect how the template is rendered in a number of ways: Genshi provides directives for conditionals and looping, among others. We’ll see some simple Genshi directives in the sections on Editing pages and Adding views.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="master.html" />
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title>Welcome to TurboGears 2.1, standing on the
shoulders of giants, since 2007</title>
</head>
<body>
${sidebar_top()}
<div id="getting_started">
<h2>Presentation</h2>
<p>TurboGears 2 is rapid web application development toolkit designed to make your life easier.</p>
<ol id="getting_started_steps">
<li class="getting_started">
<h3>Code your data model</h3>
<p> Design your data model, Create the database, and Add some bootstrap data.</p>
</li>
<li class="getting_started">
<h3>Design your URL architecture</h3>
<p> Decide your URLs, Program your controller methods, Design your
templates, and place some static files (CSS and/or JavaScript). </p>
</li>
<li class="getting_started">
<h3>Distribute your app</h3>
<p> Test your source, Generate project documents, Build a distribution.</p>
</li>
</ol>
</div>
<div class="clearingdiv" />
<div class="notice"> Thank you for choosing TurboGears.
</div>
</body>
</html>
quickstart produced a directory for our model in Wiki-20/wiki20/model/. This directory contains an __init__.py file, which makes that directory name into a python module (so you can use import model).
Since a wiki is basically a linked collection of pages, we’ll define a Page class as the name of our model. Create a new file called page.py in the Wiki-20/wiki20/model/ directory:
# -*- coding: utf-8 -*-
"""Wiki Page module."""
from sqlalchemy import *
from sqlalchemy.orm import mapper, relation
from sqlalchemy import Table, ForeignKey, Column
from sqlalchemy.types import Integer, Text
#from sqlalchemy.orm import relation, backref
from wiki20.model import DeclarativeBase, metadata, DBSession
class Page(DeclarativeBase):
__tablename__ = 'page'
##{B:Columns}
id = Column(Integer, primary_key=True)
pagename = Column(Text, unique=True)
data = Column(Text)
##{E:Columns}
def __init__(self, pagename, data):
self.pagename = pagename
self.data = data
In order to easily use our model within the application, modify the Wiki-20/wiki20/model/__init__.py file to add Page to the module. Add the following line at the end of the file:.
from wiki20.model.page import Page
Warning
It’s very important that this line is at the end because Page requires the rest of the model to be initialized before it can be imported:
Let’s investigate our model a little more. The metadata object is automatically created by the paste command inside the __init__.py file. It’s a “single point of truth” that keeps all the information necessary to connect to and use the database. It includes the location of the database, connection information and the tables that are in that database. When you pass the metadata object to the various objects in your project they initialize themselves using that metadata.
In this case, the metadata object configures itself using the development.ini file, which we’ll look at in the next section.
The SQLAlchemy DeclarativeBase object defines what a single Python object looks like in the database, and adds any necessary constraints (so, for example, even if your database doesn’t enforce uniqueness, SQLAlchemy will attempt to do so). It provides the metadata object mentioned above, and makes it very easy to define mappings from objects to tables in your database.
An object defined using the DeclarativeBase has a set of class level variables (instead of instance level) which define the columns. As you can see, Column objects are defined in the same way that you define them within a database: name, type, and constraints.
Note that it’s also possible to start with an existing database, but that’s a more advanced topic that we won’t cover in this tutorial. If you would like more information on how to do that, check out sqlautocode.
Todo
add internal links to sqlautocode when ready.
By default, projects created with quickstart are configured to use a very simple SQLite database (however, TurboGears 2 supports most popular databases). This configuration is controlled by the development.ini file in the root directory (Wiki-20, for our project).
Search down until you find the [app:main] section in development.ini, and then look for sqlalchemy.url. You should see this:
sqlalchemy.url = sqlite:///%(here)s/devdata.db
Turbogears will automatically replace the %(here)s variable with the parent directory of this file, so for our example it will produce sqlite:///Wiki-20/devdata.db. You won’t see the devdata.db file now because we haven’t yet initialized the database.
Before you can use your database, you need to initialize it and add some data. There’s built in support for this in TurboGears using paster setup-app. The quickstart template gives you a basic template database setup inside the websetup/boostrap.py file which by default creates two users, one manager group and one manage permission:
We need to update the file to create our FrontPage data just before the DBSession.flush() command by adding:
page = model.Page("FrontPage", "initial data")
model.DBSession.add(page)
The resulting boostrap file will look like:
# -*- coding: utf-8 -*-
"""Setup the Wiki-20 application"""
import logging
from tg import config
from wiki20 import model
import transaction
def bootstrap(command, conf, vars):
"""Place any commands to setup wiki20 here"""
# <websetup.bootstrap.before.auth
from sqlalchemy.exc import IntegrityError
try:
u = model.User()
u.user_name = u'manager'
u.display_name = u'Example manager'
u.email_address = u'manager@somedomain.com'
u.password = u'managepass'
model.DBSession.add(u)
g = model.Group()
g.group_name = u'managers'
g.display_name = u'Managers Group'
g.users.append(u)
model.DBSession.add(g)
p = model.Permission()
p.permission_name = u'manage'
p.description = u'This permission give an administrative right to the bearer'
p.groups.append(g)
model.DBSession.add(p)
u1 = model.User()
u1.user_name = u'editor'
u1.display_name = u'Example editor'
u1.email_address = u'editor@somedomain.com'
u1.password = u'editpass'
model.DBSession.add(u1)
page = model.Page("FrontPage", "initial data")
model.DBSession.add(page)
model.DBSession.flush()
transaction.commit()
except IntegrityError:
print 'Warning, there was a problem adding your auth data, it may have already been added:'
import traceback
print traceback.format_exc()
transaction.abort()
print 'Continuing with bootstrapping...'
# <websetup.bootstrap.after.auth>
If you’re familiar with SQLAlchemy this should look pretty standard to you. One thing to note is that we use:
transaction.commit()
Where you’re used to seeing DBSession.commit() we use transaction.commit(). This calls the transaction manager which helps us to support cross database transactions, as well as transactions in non relational databases, but ultimately in the case of SQLAlchemy it calls DBSession.commit() just like you might if you were doing it directly.
Now run the paster setup-app command:
$ paster setup-app development.ini
You’ll see output, but you should not see error messages. At this point your database is created and has some initial data in it, which you can verify by looking at Wiki-20/devdata.db. The file should exist and have a nonzero size.
That takes care of the “M” in MVC. Next is the “C”: controllers.
Controllers are the code that figures out which page to display, what data to grab from the model, how to process it, and finally hands off that processed data to a template.
quickstart has already created some basic controller code for us at Wiki-20/wiki20/controllers/root.py. Here’s what it looks like now:
# -*- coding: utf-8 -*-
"""Main Controller"""
from tg import expose, flash, require, url, request, redirect
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from tgext.admin.tgadminconfig import TGAdminConfig
from tgext.admin.controller import AdminController
from repoze.what import predicates
from wiki20.lib.base import BaseController
from wiki20.model import DBSession, metadata
from wiki20 import model
from wiki20.controllers.secure import SecureController
from wiki20.controllers.error import ErrorController
__all__ = ['RootController']
class RootController(BaseController):
"""
The root controller for the Wiki-20 application.
All the other controllers and WSGI applications should be mounted on this
controller. For example::
panel = ControlPanelController()
another_app = AnotherWSGIApplication()
Keep in mind that WSGI applications shouldn't be mounted directly: They
must be wrapped around with :class:`tg.controllers.WSGIAppController`.
"""
secc = SecureController()
admin = AdminController(model, DBSession, config_type=TGAdminConfig)
error = ErrorController()
@expose('wiki20.templates.index')
def index(self):
"""Handle the front-page."""
return dict(page='index')
@expose('wiki20.templates.about')
def about(self):
"""Handle the 'about' page."""
return dict(page='about')
@expose('wiki20.templates.environ')
def environ(self):
"""This method showcases TG's access to the wsgi environment."""
return dict(environment=request.environ)
@expose('wiki20.templates.data')
@expose('json')
def data(self, **kw):
"""This method showcases how you can use the same controller for a data page and a display page"""
return dict(params=kw)
@expose('wiki20.templates.authentication')
def auth(self):
"""Display some information about auth* on this application."""
return dict(page='auth')
@expose('wiki20.templates.index')
@require(predicates.has_permission('manage', msg=l_('Only for managers')))
def manage_permission_only(self, **kw):
"""Illustrate how a page for managers only works."""
return dict(page='managers stuff')
@expose('wiki20.templates.index')
@require(predicates.is_user('editor', msg=l_('Only for the editor')))
def editor_user_only(self, **kw):
"""Illustrate how a page exclusive for the editor works."""
return dict(page='editor stuff')
@expose('wiki20.templates.login')
def login(self, came_from=url('/')):
"""Start the user login."""
login_counter = request.environ['repoze.who.logins']
if login_counter > 0:
flash(_('Wrong credentials'), 'warning')
return dict(page='login', login_counter=str(login_counter),
came_from=came_from)
@expose()
def post_login(self, came_from='/'):
"""
Redirect the user to the initially requested page on successful
authentication or redirect her back to the login page if login failed.
"""
if not request.identity:
login_counter = request.environ['repoze.who.logins'] + 1
redirect('/login',
params=dict(came_from=came_from, __logins=login_counter))
userid = request.identity['repoze.who.userid']
flash(_('Welcome back, %s!') % userid)
redirect(came_from)
@expose()
def post_logout(self, came_from=url('/')):
"""
Redirect the user to the initially requested page on logout and say
goodbye as well.
"""
flash(_('We hope to see you soon!'))
redirect(came_from)
First, we must import the Page class from our model. At the end of the import block, add this line:
from wiki20.model.page import Page
Now we will change the template used to present the data, by changing the @expose('wiki20.templates.index') line to:
@expose('wiki20.templates.page')
This requires us to create a new template named page.html in the wiki20/templates directory; we’ll do this in the next section.
Now we must specify which page we want to see. To do this, add a parameter to the index() method. Change the line after the @expose decorator to:
def index(self, pagename="FrontPage"):
This tells the index() method to accept a parameter called pagename, with a default value of "FrontPage".
Now let’s get that page from our data model. Put this line in the body of index:
page = DBSession.query(Page).filter_by(pagename=pagename).one()
This line asks the SQLAlchemy database session object to run a query for records with a pagename column equal to the value of the pagename parameter passed to our controller method. The .one() method assures that there is only one returned result; normally a .query call returns a list of matching objects. We only want one page, so we use .one().
Finally, we need to return a dictionary containing the page we just looked up. When we say:
return dict(wikipage=page)
The returned dict will create a template variable called wikipage that will evaluate to the page object that we looked it up.
Here’s the whole file after incorporating the above modifications:
# -*- coding: utf-8 -*-
"""Main Controller"""
from tg import expose, flash, require, url, request, redirect
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from tgext.admin.tgadminconfig import TGAdminConfig
from tgext.admin.controller import AdminController
from repoze.what import predicates
from wiki20.lib.base import BaseController
from wiki20.model import DBSession, metadata
from wiki20 import model
from wiki20.controllers.secure import SecureController
from wiki20.controllers.error import ErrorController
from wiki20.model.page import Page
__all__ = ['RootController']
class RootController(BaseController):
"""
The root controller for the Wiki-20 application.
All the other controllers and WSGI applications should be mounted on this
controller. For example::
panel = ControlPanelController()
another_app = AnotherWSGIApplication()
Keep in mind that WSGI applications shouldn't be mounted directly: They
must be wrapped around with :class:`tg.controllers.WSGIAppController`.
"""
secc = SecureController()
admin = AdminController(model, DBSession, config_type=TGAdminConfig)
error = ErrorController()
@expose('wiki20.templates.page')
def index(self, pagename="FrontPage"):
"""Handle the front-page."""
page = DBSession.query(Page).filter_by(pagename=pagename).one()
return dict(wikipage=page)
@expose('wiki20.templates.about')
def about(self):
"""Handle the 'about' page."""
return dict(page='about')
@expose('wiki20.templates.environ')
def environ(self):
"""This method showcases TG's access to the wsgi environment."""
return dict(environment=request.environ)
@expose('wiki20.templates.data')
@expose('json')
def data(self, **kw):
"""This method showcases how you can use the same controller for a data page and a display page"""
return dict(params=kw)
@expose('wiki20.templates.authentication')
def auth(self):
"""Display some information about auth* on this application."""
return dict(page='auth')
@expose('wiki20.templates.index')
@require(predicates.has_permission('manage', msg=l_('Only for managers')))
def manage_permission_only(self, **kw):
"""Illustrate how a page for managers only works."""
return dict(page='managers stuff')
@expose('wiki20.templates.index')
@require(predicates.is_user('editor', msg=l_('Only for the editor')))
def editor_user_only(self, **kw):
"""Illustrate how a page exclusive for the editor works."""
return dict(page='editor stuff')
@expose('wiki20.templates.login')
def login(self, came_from=url('/')):
"""Start the user login."""
login_counter = request.environ['repoze.who.logins']
if login_counter > 0:
flash(_('Wrong credentials'), 'warning')
return dict(page='login', login_counter=str(login_counter),
came_from=came_from)
@expose()
def post_login(self, came_from='/'):
"""
Redirect the user to the initially requested page on successful
authentication or redirect her back to the login page if login failed.
"""
if not request.identity:
login_counter = request.environ['repoze.who.logins'] + 1
redirect('/login',
params=dict(came_from=came_from, __logins=login_counter))
userid = request.identity['repoze.who.userid']
flash(_('Welcome back, %s!') % userid)
redirect(came_from)
@expose()
def post_logout(self, came_from=url('/')):
"""
Redirect the user to the initially requested page on logout and say
goodbye as well.
"""
flash(_('We hope to see you soon!'))
redirect(came_from)
Now our index() method fetches a record from the database (creating an instance of our mapped Page class along the way), and returns it to the template within a dictionary.
quickstart also created some templates for us in the Wiki-20/wiki20/templates directory: master.html and index.html. Back in our simple controller, we used @expose() to hand off a dictionary of data to a template called 'wiki20.templates.index', which corresponds to Wiki-20/wiki20/templates/index.html.
Take a look at the following line in index.html:
<xi:include href="master.html" />
This tells the index template to include the master template. Using includes lets you easily maintain a cohesive look and feel throughout your site by having each page include a common master template.
Similarly the lines:
<xi:include href="header.html" />
<xi:include href="footer.html" />
Tell Genshi to pull in the headers and footers for the page.
Copy the contents of index.html into a new file called page.html. Now modify it for our purposes:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="master.html" />
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title>${wikipage.pagename} - The TurboGears 2 Wiki</title>
</head>
<body>
<div class="main_content">
<div style="float:right; width: 10em;"> Viewing
<!-- ##{B:PageName} -->
<span py:replace="wikipage.pagename">Page Name Goes Here</span>
<!-- ##{E:PageName} -->
<br/>
You can return to the <a href="/">FrontPage</a>.
</div>
<!-- ##{B:PageContent} -->
<div py:replace="wikipage.data">Page text goes here.</div>
<!-- ##{E:PageContent} -->
<div>
<a href="/edit/${wikipage.pagename}">Edit this page</a>
</div>
</div>
</body>
</html>
This is a basic XHTML page with three substitutions:
In the <title> tag, we substitute the name of the page, using the pagename value of page. (Remember, wikipage is an instance of our mapped Page class, which was passed in a dictionary by our controller.)
In the second <div> element, we substitute the page name again with Genshi’s py:replace:
<span py:replace="wikipage.pagename">Page Name Goes Here</span>
In the third <div>, we put in the contents of our``wikipage``:
<div py:replace="wikipage.data">Page text goes here.</div>
When you refresh the output web page you should see “initial data” displayed on the page.
Note
py.replace replaces the entire tag (including start and end tags) with the value of the variable provided.
For the curious...
Do you wonder what those html comments like ##{B:PageContent} are? They do not matter for this tutorial and are only to help the documentation (you’re soaking in it!) isolate certain lines of code to display, like above.
One of the fundamental features of a wiki is the ability to edit the page just by clicking “Edit This Page,” so we’ll create a template for editing. First, make a copy of page.html:
cd wiki20/templates
cp page.html edit.html
We need to replace the content with an editing form and ensure people know this is an editing page. Here are the changes for edit.html.
Change the title in the header to reflect that we are editing the page:
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title>Editing: ${wikipage.pagename}</title>
</head>
Change the div that displays the page:
<div py:replace="wikipage.data">Page text goes here.</div>
with a div that contains a standard HTML form:
<div>
<form action="/save" method="post">
<input type="hidden" name="pagename" value="${wikipage.pagename}"/>
<textarea name="data" py:content="wikipage.data" rows="10" cols="60"/>
<input type="submit" name="submit" value="Save"/>
</form>
</div>
</div>
Now that we have our view, we need to update our controller in order to display the form and handle the form submission. For displaying the form, we’ll add an edit method to our controller in Wiki-20/wiki20/controllers/root.py. The new root.py file looks like this:
# -*- coding: utf-8 -*-
"""Main Controller"""
from tg import expose, flash, require, url, request, redirect
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from tgext.admin.tgadminconfig import TGAdminConfig
from tgext.admin.controller import AdminController
from repoze.what import predicates
from wiki20.lib.base import BaseController
from wiki20.model import DBSession, metadata
from wiki20 import model
from wiki20.controllers.secure import SecureController
from wiki20.controllers.error import ErrorController
from wiki20.model.page import Page
__all__ = ['RootController']
class RootController(BaseController):
"""
The root controller for the Wiki-20 application.
All the other controllers and WSGI applications should be mounted on this
controller. For example::
panel = ControlPanelController()
another_app = AnotherWSGIApplication()
Keep in mind that WSGI applications shouldn't be mounted directly: They
must be wrapped around with :class:`tg.controllers.WSGIAppController`.
"""
secc = SecureController()
admin = AdminController(model, DBSession, config_type=TGAdminConfig)
error = ErrorController()
@expose('wiki20.templates.page')
def index(self, pagename="FrontPage"):
"""Handle the front-page."""
page = DBSession.query(Page).filter_by(pagename=pagename).one()
return dict(wikipage=page)
@expose(template="wiki20.templates.edit")
def edit(self, pagename):
page = DBSession.query(Page).filter_by(pagename=pagename).one()
return dict(wikipage=page)
@expose('wiki20.templates.about')
def about(self):
"""Handle the 'about' page."""
return dict(page='about')
@expose('wiki20.templates.environ')
def environ(self):
"""This method showcases TG's access to the wsgi environment."""
return dict(environment=request.environ)
@expose('wiki20.templates.data')
@expose('json')
def data(self, **kw):
"""This method showcases how you can use the same controller for a data page and a display page"""
return dict(params=kw)
@expose('wiki20.templates.authentication')
def auth(self):
"""Display some information about auth* on this application."""
return dict(page='auth')
@expose('wiki20.templates.index')
@require(predicates.has_permission('manage', msg=l_('Only for managers')))
def manage_permission_only(self, **kw):
"""Illustrate how a page for managers only works."""
return dict(page='managers stuff')
@expose('wiki20.templates.index')
@require(predicates.is_user('editor', msg=l_('Only for the editor')))
def editor_user_only(self, **kw):
"""Illustrate how a page exclusive for the editor works."""
return dict(page='editor stuff')
@expose('wiki20.templates.login')
def login(self, came_from=url('/')):
"""Start the user login."""
login_counter = request.environ['repoze.who.logins']
if login_counter > 0:
flash(_('Wrong credentials'), 'warning')
return dict(page='login', login_counter=str(login_counter),
came_from=came_from)
@expose()
def post_login(self, came_from='/'):
"""
Redirect the user to the initially requested page on successful
authentication or redirect her back to the login page if login failed.
"""
if not request.identity:
login_counter = request.environ['repoze.who.logins'] + 1
redirect('/login',
params=dict(came_from=came_from, __logins=login_counter))
userid = request.identity['repoze.who.userid']
flash(_('Welcome back, %s!') % userid)
redirect(came_from)
@expose()
def post_logout(self, came_from=url('/')):
"""
Redirect the user to the initially requested page on logout and say
goodbye as well.
"""
flash(_('We hope to see you soon!'))
redirect(came_from)
For now, the new method is identical to the index method; the only difference is that the resulting dictionary is handed to the edit template. To see it work, go to http://localhost:8080/edit/FrontPage . However, this only works because FrontPage already exists in our database; if you try to edit a new page with a different name it will fail, which we’ll fix in a later section.
Don’t click that save button yet! We still need to write that method.
When we displayed our wiki’s edit form in the last section, the form’s action was /save. So, we need to make a method called save in the Root class of our controller.
However, we’re also going to make another important change. Our index method is only called when you either go to / or /index. If you change the index method to the special method _default, then _default will be automatically called whenever nothing else matches. _default will take the rest of the URL and turn it into positional parameters. This will cause the wiki to become the default when possible.
Here’s our new version of root.py which includes both default and save:
# -*- coding: utf-8 -*-
"""Main Controller"""
from tg import expose, flash, require, url, request, redirect
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from tgext.admin.tgadminconfig import TGAdminConfig
from tgext.admin.controller import AdminController
from repoze.what import predicates
from wiki20.lib.base import BaseController
from wiki20.model import DBSession, metadata
from wiki20 import model
from wiki20.controllers.secure import SecureController
from wiki20.controllers.error import ErrorController
from wiki20.model.page import Page
__all__ = ['RootController']
class RootController(BaseController):
"""
The root controller for the Wiki-20 application.
All the other controllers and WSGI applications should be mounted on this
controller. For example::
panel = ControlPanelController()
another_app = AnotherWSGIApplication()
Keep in mind that WSGI applications shouldn't be mounted directly: They
must be wrapped around with :class:`tg.controllers.WSGIAppController`.
"""
secc = SecureController()
admin = AdminController(model, DBSession, config_type=TGAdminConfig)
error = ErrorController()
@expose('wiki20.templates.page')
def _default(self, pagename="FrontPage"):
"""Handle the front-page."""
page = DBSession.query(Page).filter_by(pagename=pagename).one()
return dict(wikipage=page)
@expose(template="wiki20.templates.edit")
def edit(self, pagename):
page = DBSession.query(Page).filter_by(pagename=pagename).one()
return dict(wikipage=page)
@expose()
def save(self, pagename, data, submit):
page = DBSession.query(Page).filter_by(pagename=pagename).one()
page.data = data
redirect("/" + pagename)
@expose('wiki20.templates.about')
def about(self):
"""Handle the 'about' page."""
return dict(page='about')
@expose('wiki20.templates.environ')
def environ(self):
"""This method showcases TG's access to the wsgi environment."""
return dict(environment=request.environ)
@expose('wiki20.templates.data')
@expose('json')
def data(self, **kw):
"""This method showcases how you can use the same controller for a data page and a display page"""
return dict(params=kw)
@expose('wiki20.templates.authentication')
def auth(self):
"""Display some information about auth* on this application."""
return dict(page='auth')
@expose('wiki20.templates.index')
@require(predicates.has_permission('manage', msg=l_('Only for managers')))
def manage_permission_only(self, **kw):
"""Illustrate how a page for managers only works."""
return dict(page='managers stuff')
@expose('wiki20.templates.index')
@require(predicates.is_user('editor', msg=l_('Only for the editor')))
def editor_user_only(self, **kw):
"""Illustrate how a page exclusive for the editor works."""
return dict(page='editor stuff')
@expose('wiki20.templates.login')
def login(self, came_from=url('/')):
"""Start the user login."""
login_counter = request.environ['repoze.who.logins']
if login_counter > 0:
flash(_('Wrong credentials'), 'warning')
return dict(page='login', login_counter=str(login_counter),
came_from=came_from)
@expose()
def post_login(self, came_from='/'):
"""
Redirect the user to the initially requested page on successful
authentication or redirect her back to the login page if login failed.
"""
if not request.identity:
login_counter = request.environ['repoze.who.logins'] + 1
redirect('/login',
params=dict(came_from=came_from, __logins=login_counter))
userid = request.identity['repoze.who.userid']
flash(_('Welcome back, %s!') % userid)
redirect(came_from)
@expose()
def post_logout(self, came_from=url('/')):
"""
Redirect the user to the initially requested page on logout and say
goodbye as well.
"""
flash(_('We hope to see you soon!'))
redirect(came_from)
Unlike the previous methods we’ve made, save just uses a plain @expose() without any template specified. That’s because we’re only redirecting the user back to the viewing page.
Although the page.data = data statement tells SQLAlchemy that you intend to store the page data in the database, nothing happens until the DBSession.flush() method is called. This is commonly referred to as the “unit of work” pattern, and it’s an important structure for database developers because it allows SQLAlchemy to combine many operations into a single database update (or a minimized number of updates if some changes depend upon earlier changes) and thus be much more efficient in the database resources used.
SQLAlchemy also provides a DBSession.commit() method which flushes and commits any changes you’ve made in a transaction. TurboGears 2 provides a flexible transaction management system that automates this process wrapping each web request in its own transaction and automatically rolling back that transaction if you get a python exception, or return an HTTP error code as your response.
You don’t have to do anything to use this transaction management system, it should just work. So, you can now make changes and save the page we were editing, just like a real wiki.
Our wiki doesn’t yet have a way to link pages. A typical wiki will automatically create links for WikiWords when it finds them (WikiWords have also been described as WordsSmashedTogether). This sounds like a job for a regular expression.
Here’s the new version of root.py, which will be explained afterwards:
# -*- coding: utf-8 -*-
"""Main Controller"""
from tg import expose, flash, require, url, request, redirect
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from tgext.admin.tgadminconfig import TGAdminConfig
from tgext.admin.controller import AdminController
from repoze.what import predicates
from wiki20.lib.base import BaseController
from wiki20.model import DBSession, metadata
from wiki20 import model
from wiki20.controllers.secure import SecureController
from wiki20.controllers.error import ErrorController
from wiki20.model.page import Page
import re
from docutils.core import publish_parts
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
__all__ = ['RootController']
class RootController(BaseController):
"""
The root controller for the Wiki-20 application.
All the other controllers and WSGI applications should be mounted on this
controller. For example::
panel = ControlPanelController()
another_app = AnotherWSGIApplication()
Keep in mind that WSGI applications shouldn't be mounted directly: They
must be wrapped around with :class:`tg.controllers.WSGIAppController`.
"""
secc = SecureController()
admin = AdminController(model, DBSession, config_type=TGAdminConfig)
error = ErrorController()
@expose('wiki20.templates.page')
def _default(self, pagename="FrontPage"):
"""Handle the front-page."""
page = DBSession.query(Page).filter_by(pagename=pagename).one()
content = publish_parts(page.data, writer_name="html")["html_body"]
root = url('/')
content = wikiwords.sub(r'<a href="%s\1">\1</a>' % root, content)
return dict(content=content, wikipage=page)
@expose(template="wiki20.templates.edit")
def edit(self, pagename):
page = DBSession.query(Page).filter_by(pagename=pagename).one()
return dict(wikipage=page)
@expose()
def save(self, pagename, data, submit):
page = DBSession.query(Page).filter_by(pagename=pagename).one()
page.data = data
redirect("/" + pagename)
@expose('wiki20.templates.about')
def about(self):
"""Handle the 'about' page."""
return dict(page='about')
@expose('wiki20.templates.environ')
def environ(self):
"""This method showcases TG's access to the wsgi environment."""
return dict(environment=request.environ)
@expose('wiki20.templates.data')
@expose('json')
def data(self, **kw):
"""This method showcases how you can use the same controller for a data page and a display page"""
return dict(params=kw)
@expose('wiki20.templates.authentication')
def auth(self):
"""Display some information about auth* on this application."""
return dict(page='auth')
@expose('wiki20.templates.index')
@require(predicates.has_permission('manage', msg=l_('Only for managers')))
def manage_permission_only(self, **kw):
"""Illustrate how a page for managers only works."""
return dict(page='managers stuff')
@expose('wiki20.templates.index')
@require(predicates.is_user('editor', msg=l_('Only for the editor')))
def editor_user_only(self, **kw):
"""Illustrate how a page exclusive for the editor works."""
return dict(page='editor stuff')
@expose('wiki20.templates.login')
def login(self, came_from=url('/')):
"""Start the user login."""
login_counter = request.environ['repoze.who.logins']
if login_counter > 0:
flash(_('Wrong credentials'), 'warning')
return dict(page='login', login_counter=str(login_counter),
came_from=came_from)
@expose()
def post_login(self, came_from='/'):
"""
Redirect the user to the initially requested page on successful
authentication or redirect her back to the login page if login failed.
"""
if not request.identity:
login_counter = request.environ['repoze.who.logins'] + 1
redirect('/login',
params=dict(came_from=came_from, __logins=login_counter))
userid = request.identity['repoze.who.userid']
flash(_('Welcome back, %s!') % userid)
redirect(came_from)
@expose()
def post_logout(self, came_from=url('/')):
"""
Redirect the user to the initially requested page on logout and say
goodbye as well.
"""
flash(_('We hope to see you soon!'))
redirect(came_from)
We need some additional imports, including re for regular expressions and a method called publish_parts from docutils.
A WikiWord is a word that starts with an uppercase letter, has a collection of lowercase letters and numbers followed by another uppercase letter and more letters and numbers. The wikiwords regular expression describes a WikiWord.
In _default, the new lines begin with the use of publish_parts, which is a utility that takes string input and returns a dictionary of document parts after performing conversions; in our case, the conversion is from Restructured Text to HTML. The input (page.data) is in Restructured Text format, and the output format (specified by writer_name="html") is in HTML. Selecting the fragment part produces the document without the document title, subtitle, docinfo, header, and footer.
You can configure TurboGears so that it doesn’t live at the root of a site, so you can combine multiple TurboGears apps on a single server. Using tg.url() creates relative links, so that your links will continue to work regardless of how many apps you’re running.
The next line rewrites the content by finding any WikiWords and substituting hyperlinks for those WikiWords. That way when you click on a WikiWord, it will take you to that page. The r'string' means ‘raw string’, one that turns off escaping, which is mostly used in regular expression strings to prevent you from having to double escape slashes. The substitution may look a bit weird, but is more understandable if you recognize that the %s gets substituted with root, then the substitution is done which replaces the \1 with the string matching the regex.
Note that _default() is now returning a dict containing an additional key-value pair: content=content. This will not break wiki20.templates.page because that page is only looking for page in the dictionary, however if we want to do something interesting with the new key-value pair we’ll need to edit wiki20.templates.page:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="master.html" />
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title>${wikipage.pagename} - The TurboGears 2 Wiki</title>
</head>
<body>
<div class="main_content">
<div style="float:right; width: 10em;"> Viewing
<!-- ##{B:PageName} -->
<span py:replace="wikipage.pagename">Page Name Goes Here</span>
<!-- ##{E:PageName} -->
<br/>
You can return to the <a href="/">FrontPage</a>.
</div>
<!-- ##{B:PageContent} -->
<div py:replace="XML(content)">Formatted content goes here.</div>
<!-- ##{E:PageContent} -->
<div>
<a href="/edit/${wikipage.pagename}">Edit this page</a>
</div>
</div>
</body>
</html>
Since content comes through as XML, we can strip it off using the XML() function to produce plain text (try removing the function call to see what happens).
To test the new version of the system, edit the data in your front page to include a WikiWord. When the page is displayed, you’ll see that it’s now a link. You probably won’t be surprised to find that clicking that link produces an error.
What if a Wiki page doesn’t exist? We’ll take a simple approach: if the page doesn’t exist, you get an edit page to use to create it.
In the _default method, we’ll check to see if the page exists. If it doesn’t, we’ll redirect to a new notfound method. We’ll add this method after the _default method and before the edit method. Here are the changes we make to the controller:
# -*- coding: utf-8 -*-
"""Main Controller"""
from tg import expose, flash, require, url, request, redirect
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from tgext.admin.tgadminconfig import TGAdminConfig
from tgext.admin.controller import AdminController
from repoze.what import predicates
from wiki20.lib.base import BaseController
from wiki20.model import DBSession, metadata
from wiki20 import model
from wiki20.controllers.secure import SecureController
from wiki20.controllers.error import ErrorController
from wiki20.model.page import Page
import re
from docutils.core import publish_parts
from sqlalchemy.exc import InvalidRequestError
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
__all__ = ['RootController']
class RootController(BaseController):
"""
The root controller for the Wiki-20 application.
All the other controllers and WSGI applications should be mounted on this
controller. For example::
panel = ControlPanelController()
another_app = AnotherWSGIApplication()
Keep in mind that WSGI applications shouldn't be mounted directly: They
must be wrapped around with :class:`tg.controllers.WSGIAppController`.
"""
secc = SecureController()
admin = AdminController(model, DBSession, config_type=TGAdminConfig)
error = ErrorController()
@expose('wiki20.templates.page')
def _default(self, pagename="FrontPage"):
"""Handle the front-page."""
try:
page = DBSession.query(Page).filter_by(pagename=pagename).one()
except InvalidRequestError:
raise redirect("notfound", pagename=pagename)
content = publish_parts(page.data, writer_name="html")["html_body"]
root = url('/')
content = wikiwords.sub(r'<a href="%s\1">\1</a>' % root, content)
return dict(content=content, wikipage=page)
@expose("wiki20.templates.edit")
def notfound(self, pagename):
page = Page(pagename=pagename, data="")
DBSession.add(page)
return dict(wikipage=page)
@expose(template="wiki20.templates.edit")
def edit(self, pagename):
page = DBSession.query(Page).filter_by(pagename=pagename).one()
return dict(wikipage=page)
@expose()
def save(self, pagename, data, submit):
page = DBSession.query(Page).filter_by(pagename=pagename).one()
page.data = data
redirect("/" + pagename)
@expose('wiki20.templates.about')
def about(self):
"""Handle the 'about' page."""
return dict(page='about')
@expose('wiki20.templates.environ')
def environ(self):
"""This method showcases TG's access to the wsgi environment."""
return dict(environment=request.environ)
@expose('wiki20.templates.data')
@expose('json')
def data(self, **kw):
"""This method showcases how you can use the same controller for a data page and a display page"""
return dict(params=kw)
@expose('wiki20.templates.authentication')
def auth(self):
"""Display some information about auth* on this application."""
return dict(page='auth')
@expose('wiki20.templates.index')
@require(predicates.has_permission('manage', msg=l_('Only for managers')))
def manage_permission_only(self, **kw):
"""Illustrate how a page for managers only works."""
return dict(page='managers stuff')
@expose('wiki20.templates.index')
@require(predicates.is_user('editor', msg=l_('Only for the editor')))
def editor_user_only(self, **kw):
"""Illustrate how a page exclusive for the editor works."""
return dict(page='editor stuff')
@expose('wiki20.templates.login')
def login(self, came_from=url('/')):
"""Start the user login."""
login_counter = request.environ['repoze.who.logins']
if login_counter > 0:
flash(_('Wrong credentials'), 'warning')
return dict(page='login', login_counter=str(login_counter),
came_from=came_from)
@expose()
def post_login(self, came_from='/'):
"""
Redirect the user to the initially requested page on successful
authentication or redirect her back to the login page if login failed.
"""
if not request.identity:
login_counter = request.environ['repoze.who.logins'] + 1
redirect('/login',
params=dict(came_from=came_from, __logins=login_counter))
userid = request.identity['repoze.who.userid']
flash(_('Welcome back, %s!') % userid)
redirect(came_from)
@expose()
def post_logout(self, came_from=url('/')):
"""
Redirect the user to the initially requested page on logout and say
goodbye as well.
"""
flash(_('We hope to see you soon!'))
redirect(came_from)
The _default code changes illustrate the “better to beg forgiveness than ask permission” pattern which is favored by most Pythonistas – we first try to get the page and then deal with the exception by redirecting to a method that will make a new page.
We’re also leaking a bit of our model into our controller. For a larger project, we might create a facade in the model, but here we’ll favor simplicity. Notice that we can use the redirect() to pass parameters into the destination method.
As for the notfound method, the first two lines of the method add a row to the page table. From there, the path is exactly the same it would be for our edit method.
With these changes in place, we have a fully functional wiki. Give it a try! You should be able to create new pages now.
Most wikis have a feature that lets you view an index of the pages. To add one, we’ll start with a new template, pagelist.html. We’ll copy page.html so that we don’t have to write the boilerplate.
cd wiki20/templates
cp page.html pagelist.html
After editing, our pagelist.html looks like:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="header.html" />
<xi:include href="sidebars.html" />
<xi:include href="footer.html" />
<xi:include href="master.html" />
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title>Page Listing - The TurboGears 2 Wiki</title>
</head>
<body>
<div class="main_content">
<h1>All Pages</h1>
<ul>
<li py:for="pagename in pages">
<a href="${tg.url('/' + pagename)}"
py:content="pagename">Page Name Here.</a>
</li>
</ul>
Return to the <a href="/">FrontPage</a>.
</div>
</body>
</html>
The section in bold represents the Genshi code of interest. You can guess that the py:for is a python for loop, modified to fit into Genshi’s XML. It iterates through each of the pages (which we’ll send in via the controller, using a modification you’ll see next). For each one, Page Name Here is replaced by pagename, as is the URL. You can learn more about the Genshi templating engine at their site.
We must also modify the controller to implement pagelist and to create and pass pages to our template:
# -*- coding: utf-8 -*-
"""Main Controller"""
from tg import expose, flash, require, url, request, redirect
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from tgext.admin.tgadminconfig import TGAdminConfig
from tgext.admin.controller import AdminController
from repoze.what import predicates
from wiki20.lib.base import BaseController
from wiki20.model import DBSession, metadata
from wiki20 import model
from wiki20.controllers.secure import SecureController
from wiki20.controllers.error import ErrorController
from wiki20.model.page import Page
import re
from docutils.core import publish_parts
from sqlalchemy.exc import InvalidRequestError
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
__all__ = ['RootController']
class RootController(BaseController):
"""
The root controller for the Wiki-20 application.
All the other controllers and WSGI applications should be mounted on this
controller. For example::
panel = ControlPanelController()
another_app = AnotherWSGIApplication()
Keep in mind that WSGI applications shouldn't be mounted directly: They
must be wrapped around with :class:`tg.controllers.WSGIAppController`.
"""
secc = SecureController()
admin = AdminController(model, DBSession, config_type=TGAdminConfig)
error = ErrorController()
@expose('wiki20.templates.page')
def _default(self, pagename="FrontPage"):
"""Handle the front-page."""
try:
page = DBSession.query(Page).filter_by(pagename=pagename).one()
except InvalidRequestError:
raise redirect("notfound", pagename=pagename)
content = publish_parts(page.data, writer_name="html")["html_body"]
root = url('/')
content = wikiwords.sub(r'<a href="%s\1">\1</a>' % root, content)
return dict(content=content, wikipage=page)
@expose("wiki20.templates.edit")
def notfound(self, pagename):
page = Page(pagename=pagename, data="")
DBSession.add(page)
return dict(wikipage=page)
@expose(template="wiki20.templates.edit")
def edit(self, pagename):
page = DBSession.query(Page).filter_by(pagename=pagename).one()
return dict(wikipage=page)
@expose()
def save(self, pagename, data, submit):
page = DBSession.query(Page).filter_by(pagename=pagename).one()
page.data = data
redirect("/" + pagename)
@expose("wiki20.templates.pagelist")
def pagelist(self):
pages = [page.pagename for page in DBSession.query(Page).order_by(Page.pagename)]
return dict(pages=pages)
@expose('wiki20.templates.about')
def about(self):
"""Handle the 'about' page."""
return dict(page='about')
@expose('wiki20.templates.environ')
def environ(self):
"""This method showcases TG's access to the wsgi environment."""
return dict(environment=request.environ)
@expose('wiki20.templates.data')
@expose('json')
def data(self, **kw):
"""This method showcases how you can use the same controller for a data page and a display page"""
return dict(params=kw)
@expose('wiki20.templates.authentication')
def auth(self):
"""Display some information about auth* on this application."""
return dict(page='auth')
@expose('wiki20.templates.index')
@require(predicates.has_permission('manage', msg=l_('Only for managers')))
def manage_permission_only(self, **kw):
"""Illustrate how a page for managers only works."""
return dict(page='managers stuff')
@expose('wiki20.templates.index')
@require(predicates.is_user('editor', msg=l_('Only for the editor')))
def editor_user_only(self, **kw):
"""Illustrate how a page exclusive for the editor works."""
return dict(page='editor stuff')
@expose('wiki20.templates.login')
def login(self, came_from=url('/')):
"""Start the user login."""
login_counter = request.environ['repoze.who.logins']
if login_counter > 0:
flash(_('Wrong credentials'), 'warning')
return dict(page='login', login_counter=str(login_counter),
came_from=came_from)
@expose()
def post_login(self, came_from='/'):
"""
Redirect the user to the initially requested page on successful
authentication or redirect her back to the login page if login failed.
"""
if not request.identity:
login_counter = request.environ['repoze.who.logins'] + 1
redirect('/login',
params=dict(came_from=came_from, __logins=login_counter))
userid = request.identity['repoze.who.userid']
flash(_('Welcome back, %s!') % userid)
redirect(came_from)
@expose()
def post_logout(self, came_from=url('/')):
"""
Redirect the user to the initially requested page on logout and say
goodbye as well.
"""
flash(_('We hope to see you soon!'))
redirect(came_from)
Here, we select all of the Page objects from the database, and order them by pagename.
We can also modify page.html so that the link to the page list is available on every page:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="master.html" />
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title>${wikipage.pagename} - The TurboGears 2 Wiki</title>
</head>
<body>
<div class="main_content">
<div style="float:right; width: 10em;"> Viewing
<!-- ##{B:PageName} -->
<span py:replace="wikipage.pagename">Page Name Goes Here</span>
<!-- ##{E:PageName} -->
<br/>
You can return to the <a href="/">FrontPage</a>.
</div>
<!-- ##{B:PageContent} -->
<div py:replace="XML(content)">Formatted content goes here.</div>
<!-- ##{E:PageContent} -->
<div>
<a href="/edit/${wikipage.pagename}">Edit this page</a>
<a href="/pagelist">View the page list</a>
</div>
</div>
</body>
</html>
You can see your pagelist by clicking the link on a page or by going directly to http://localhost:8080/pagelist .
Now that you have a working Wiki, there are a number of further places to explore:
Todo
Add link to help show how to add jQuery support
If you had any problems with this tutorial, or have ideas on how to make it better, please let us know on the mailing list! Suggestions are almost always incorporated.