1 Introduction
If you haven't done so already read the installation instructions and getting started guide.
In this tutorial we are going to create a working wiki from scratch using Pylons 0.9.6 and SQLAlchemy. Our wiki will allow visitors to add, edit or delete formatted wiki pages.
2 Starting at the End
Pylons is designed to be easy for everyone, not just developers, so lets start by downloading and installing the finished QuickWiki in exactly the way end users of QuickWiki might do. Once we have explored its features we will set about writing it from scratch.
After you have installed Easy Install run these commands to install QuickWiki and create a config file:
1 2 | $ easy_install QuickWiki==0.1.5 $ paster make-config QuickWiki test.ini |
Next edit the configuration file by specifying the sqlalchemy.default.url variable in [app:main] section so that the data source name points to the database you wish to use.
Note
The default sqlite:///%(here)s/quickwiki.db uses a (file-based) SQLite database named quickwiki.db in the project's top-level directory. This database will be created for you when running the paster setup-app command below, but you could also use MySQL, Oracle or PostgreSQL. Firebird and MS-SQL may also work. See the SQLAlchemy documentation for more information on how to connect to different databases. SQLite for example requires additional forward slashes in its URI, where the client/server databases should only use two. You will also need to make sure you have the appropriate Python driver for the database you wish to use. If you're using Python 2.5, a version of the pysqlite adapter is already included, so you can jump right in with the tutorial. You may need to get SQLite itself.
Finally create the database tables and serve the finished application:
1 2 | $ paster setup-app test.ini $ paster serve test.ini |
That's it! Now you can visit http://127.0.0.1:5000 and experiment with the finished Wiki. Note that in the title list screen you can drag page titles to the trash area to delete them via AJAX calls.
When you've finished, stop the server with CTRL+C because we will start developing our own version.
If you are interested in looking at the latest version of the QuickWiki source code it can be browsed online at http://bitbucket.org/bbangert/quickwiki/ or can be checked out using Mercurial:
1 | $ hg clone http://bitbucket.org/bbangert/quickwiki/
|
Note
To run the version checked out from the repository, you'll want to run python setup.py egg_info from the project's root directory. This will generate some files in the QuickWiki.egg-info directory.
Note that there is also currently a small bug where running the command doesn't generate a paster_plugins.txt file in the egg-info directory. Without this, paster shell will not work. Create it yourself, and add the text Pylons, WebHelpers and PasteScript on separate lines. Hopefully this issue will be fixed soon.
3 Developing QuickWiki
If you skipped the "Starting at the End" section you will need to assure that you have Pylons installed. See the installation instructions, or in a nutshell:
1 | $ easy_install -U Pylons>=0.9.6 |
Then create your project:
1 | $ paster create -t pylons QuickWiki
|
Now lets start the server and see what we have:
1 2 | $ cd QuickWiki $ paster serve --reload development.ini |
Note
We have started the server with the --reload switch. This means any changes we make to code will cause the server to restart (if necessary); your changes are immediately reflected on the live site.
Open a new console and cd QuickWiki/quickwiki. Visit http://127.0.0.1:5000 where you will see the introduction page. Delete the file public/index.html because we want to see the front page of the wiki instead of this welcome page. If you now refresh the page, the Pylons built-in error document support will kick in and display an Error 404 page to tell you the file could not be found. We'll setup a controller to handle this location later.
4 The Model
Pylons uses a Model View Controller architecture; we'll start by creating the model. We could use any system we like for the model including SQLObject or SQLAlchemy. SQLAlchemy is the default for current versions of Pylons, and we'll use it for QuickWiki.
Note
SQLAlchemy is a Python SQL toolkit and Object Relational Mapper that is fast becoming the default choice for many Python programmers.
SQLAlchemy provides a full suite of well known enterprise-level persistence patterns, designed for efficient and high-performance database access, adapted into a simple and Pythonic domain language. There is full and detailed documentation available on the SQLAlchemy website at http://sqlalchemy.org/docs/ and you should really read this before you get heavily into SQLAlchemy.
The most basic way of using SQLAlchemy is with explicit sessions where you create Session objects as needed. Pylons applications typically employ a slightly more sophisticated setup using SQLAlchemy 0.4's "contextual," thread-local sessions, via scoped_session. With this configuration, the application can use a single Session instance per web request, without the need to pass it around explicitly. Instantiating a new Session will actually find an existing one in the current thread if available. This is also covered in the document Using SQLAlchemy with Pylons, and you can learn further details in the SQLAlchemy documentation on the Session.
Note
It is important to recognize the difference between SQLAlchemy's (or possibly another DB abstraction layer's) Session object and Pylons' standard session (with a lowercase 's') for web requests. See Beaker for more on the latter. It is customary to reference the database session by model.Session outside of model classes.
Now replace the contents of your model/__init__.py file so that it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from pylons import config from sqlalchemy import Column, MetaData, Table, types from sqlalchemy.orm import mapper from sqlalchemy.orm import scoped_session, sessionmaker Session = scoped_session(sessionmaker(autoflush=True, transactional=True, bind=config['pylons.g'].sa_engine)) metadata = MetaData() pages_table = Table('pages', metadata, Column('title', types.Unicode(40), primary_key=True), Column('content', types.Unicode(), default='') ) |
The first line imports Pylons' config object so we can bind our database Session to an engine -- more on that in a bit. The second line imports some useful SQLAlchemy objects such as the Table and Column classes. The third imports the mapper function which we use to map our table schemas to objects. The final import statement provides two functions for setting up the session and adding the contextual functionality.
After the imports we setup our metadata object which is used when defining and managing tables. We then define a table called pages which has two columns, title (the primary key) and content.
Note
SQLAlchemy also supports reflecting table information directly from a database. If we had already created the pages database table, SQLAlchemy could have constructed the pages_table object for us. This uses the autoload=True parameter in place of the Column definitions, like this:
1 | pages_table = Table('pages', metadata, autoload=True) |
Note
A primary key is a unique ID for each row in a database table. In the example above we are using the page title as a natural primary key. Some people prefer to use integer primary keys for all tables, so-called surrogate primary keys. The author of this tutorial uses both methods in his own code and is not advocating one method over the other, it is important that you choose the best database structure for your application. See the Pylons Cookbook for a quick general overview of relational databases if you're not familiar with these concepts.
A core philosophy of SQLAlchemy is that tables and domain classes are different beasts. So next, we'll create the Python class that will represent the pages of our wiki and map these domain objects to rows in the pages table using a mapper. In a more complex application, you could break out model classes into separate .py files in your model directory, but for sake of simplicity in this case, we'll just stick to __init__.py.
Add this to the bottom of model/__init__.py:
1 2 3 4 5 | class Page(object): def __str__(self): return self.title mapper(Page, pages_table) |
For those familiar with SQLAlchemy 0.3, scoped_session replaces the sessioncontext extension, and Session.mapper could then be used here in place of mapper to get behavior similar to what used to be achieved with assign_mapper. This is considered an advanced topic, and you should consult SQLAlchemy's documentation if you wish to learn how it works.
Looking ahead, our wiki will need some formatting so we will need to turn the content field into HTML. Any WikiWords (which are words made by joining together two or more lowercase words with the first letter capitalized) will also need to be converted into hyperlinks.
It would be nice if we could add a method to our Page object to retrieve the formatted HTML with the WikiWords already converted to hyperlinks. Add the following at the top of the model/__init__.py file:
1 2 3 4 5 6 7 8 | import re import sets from docutils.core import publish_parts import quickwiki.lib.helpers as h wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)", re.UNICODE) |
and then add a get_wiki_content() method to the Page object so it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Page(object): content = None def __str__(self): return self.title def get_wiki_content(self): content = publish_parts(self.content, writer_name="html")["html_body"] titles = sets.Set(wikiwords.findall(content)) for title in titles: title_url = h.url_for(controller='page', action='index', title=title) content = content.replace(title, h.link_to(title, title_url)) return content |
This code deserves a bit of explaining. The content = None line is so that the content attribute is initialized to None when a new Page object is created. The Page object represents a row in the pages table so self.content will be the value of the content field. The Set object provides us with only unique WikiWord names, so we don't try replacing them more than once (a "wikiword" is of course defined by the regular expression set globally). h.link_to() and h.url_for() are standard Pylons helpers which create links to specific controller actions. In this case we have decided that all WikiWords should link to the index action of the page controller which we will create later.
Note
Pylons uses a Model View Controller architecture and so the formatting of objects into HTML should usually be handled in the view, i.e. in a template. In this example converting reStructuredText into HTML in a template is not appropriate so we are treating the HTML representation of the content as part of the model. It also gives us the chance to demonstrate that SQLAlchemy domain classes are real Python classes that can have their own methods.
One final change, since we have used docutils and SQLAlchemy, both third party packages, we need to edit our setup.py file so that anyone installing QuickWiki with Easy Install will automatically also have these dependencies installed for them too. Edit your setup.py in your project root directory so that the install_requires line looks like this:
1 | install_requires=["Pylons>=0.9.6", "docutils==0.4", "SQLAlchemy>=0.4.1"], |
While we are we are making changes to setup.py we might want to complete some of the other sections too. Set the version number to 0.1.5 and add a description and URL which will be used on the Python Cheeseshop when we release it:
1 2 3 | version="0.1.5", description="QuickWiki - Pylons 0.9.6 Tutorial application", url="http://wiki.pylonshq.com/display/pylonsdocs/QuickWiki+Tutorial", |
We might also want to make a full release rather than a development release in which case we would remove the following lines from setup.cfg:
1 2 3 | [egg_info] tag_build = dev tag_svn_revision = true |
To test the automatic installation of the dependencies, run the following command which will also install docutils and SQLAlchemy if you don't already have them:
1 | $ python setup.py develop
|
Note
The command python setup.py develop installs your application in a special mode so that it behaves exactly as if it had been installed as an egg file by an end user. This is really useful when you are developing an application because it saves you having to create an egg and install it every time you want to test a change.
5 Configuration and Setup
Now lets make the changes necessary to enable QuickWiki to be set up by an end user. First, open environment.py from the config directory of your project. After from pylons import config, add the following import:
1 | from sqlalchemy import engine_from_config |
Then, add this line at the end of the load_environment function:
1 2 | config['pylons.g'].sa_engine = engine_from_config(config, 'sqlalchemy.default.') |
This creates an engine for each instance of your application, which manages connections and is the base level at which SQLAlchemy communicates with the database. The engine is added to Pylons' config object, where you earlier saw it accessed in the base parameter for setting up SQLAlchemy's Session.
Now edit websetup.py, used by the paster setup-app command, to look like this:
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 | """Setup the QuickWiki application""" import logging from paste.deploy import appconfig from pylons import config from quickwiki.config.environment import load_environment log = logging.getLogger(__name__) def setup_config(command, filename, section, vars): """Place any commands to setup quickwiki here""" conf = appconfig('config:' + filename) load_environment(conf.global_conf, conf.local_conf) # Populate the DB on 'paster setup-app' import quickwiki.model as model log.info("Setting up database connectivity...") engine = config['pylons.g'].sa_engine log.info("Creating tables...") model.metadata.create_all(bind=engine) log.info("Successfully set up.") log.info("Adding front page data...") page = model.Page() page.title = 'FrontPage' page.content = 'Welcome to the QuickWiki front page.' model.Session.save(page) model.Session.commit() log.info("Successfully set up.") |
You can see that environment.py's load_environment function is called, so our engine is ready and we can import the model. A SQLAlchemy MetaData object--which provides some utility methods for operating on database schema--usually needs to be connected to an engine, so the line model.metadata.create_all(bind=engine) uses the engine we've set up and, well, creates the table(s) we've defined. After the tables are created the other lines add some data for the simple front page to our wiki. Because we specified transactional=True when creating our Session, operations will be wrapped in a transaction and committed atomically (unless your DB doesn't support transactions, like MySQL's default MyISAM tables -- but that's beyond the scope of this tutorial).
To test this functionality run you first need to install your QuickWiki if you haven't already done so in order for paster to find the version we are developing instead of the version we installed at the very start:
1 | $ python setup.py develop
|
Specify your database URI in development.ini so that the [app:main] section contains something like this, customized as needed for your database:
1 2 3 4 5 6 7 | [app:main] use = egg:QuickWiki ... # Specify the database for SQLAlchemy to use. # %(here) may include a ':' character on Windows environments; this can # invalidate the URI when specifying a SQLite db via path name sqlalchemy.default.url = sqlite:///%(here)s/quickwiki.db |
Note
See the SQLAlchemy note in the Starting at the End section for information on supported database URIs and a link to the SQLAlchemy documentation about the various options that can be included in them.
If you want to see the SQL being generated, you can have SQLAlchemy echo it to the console by adding this line:
1 | sqlalchemy.default.echo = true |
You can now run the paster setup-app command to setup your tables in the same way an end user would, remembering to drop and recreate the database if the version tested earlier has already created the tables:
1 | $ paster setup-app development.ini
|
At this stage you will need to ensure you have the appropriate Python database drivers for the database you chose, otherwise you might find SQLAlchemy complains it can't get the DBAPI module for the dialect it needs.
You should also edit QuickWiki.egg-info/paste_deploy_config.ini_tmpl so that when users run paster make-config the configuration file that is produced for them will already have a section telling them to enter their own database URI as we did when we installed the finished QuickWiki at the start of the tutorial. Add these lines in the [app:main] section:
1 2 3 4 5 | # Specify the database for SQLAlchemy to use. # %(here) may include a ':' character on Windows environments; this can # invalidate the URI when specifying a SQLite db via path name #sqlalchemy.default.url = sqlite:///%(here)s/quickwiki.db #sqlalchemy.default.echo = true |
6 Templates
Note
Pylons uses the Mako templating language by default, although as is the case with most aspects of Pylons you are free to deviate from the default if you prefer. Pylons also supports Genshi, Kid and Cheetah out of the box.
We will make use of a feature of the Mako templating language called inheritance for our project. Add the main page template in templates/base.mako:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html> <head> <title>QuickWiki</title> ${h.stylesheet_link_tag('/quick.css')} ${h.javascript_include_tag('/javascripts/effects.js', builtins=True)} </head> <body> <div class="content"> ${next.body()}\ <p class="footer"> Return to the ${h.link_to('FrontPage', h.url_for(action="index", title="FrontPage"))} | ${h.link_to('Edit ' + c.title, h.url_for(title=c.title, action='edit'))} </p> </div> </body> </html> |
All our other templates will be automatically inserted into the ${next.body()} line and the whole page will be returned when we call the render() global from our controller so that we can easily apply a consistent theme to all our templates.
If you are interested in learning some of the features of Mako templates have a look at the comprehensive Mako Documentation. For now we just need to understand that next.body() is replaced with the child template and that anything within ${...} brackets is executed and replaced with the result.
This base.mako also makes use of various helper functions attached to the h object. These are described in the WebHelpers documentation. You can add more helpers to the h object by adding them to lib/helpers.py although for this project we don't need to do so.
7 Routing
Before we can add the actions we want to be able to route the requests to them correctly. Edit config/routing.py and adjust the 'Custom Routes' section to look like this:
1 2 3 4 | map.connect(':controller/:action/:title', controller='page', action='index', title='FrontPage') map.connect(':title', controller='page', action='index', title='FrontPage') map.connect('*url', controller='template', action='view') |
Note that the default route has been replaced. This tells Pylons to route the root URL / to the index() method of the PageController class in page.py and specify the title argument as FrontPage. It also says that any URL of the form /SomePage should be routed to the same method but the title argument will contain the value of the first part of the URL, in this case SomePage. Any other URLs which can't be matched by these maps are routed to the template controller as usual where they will result in a 404 error page being displayed.
One of the main benefits of using the Routes system is that you can also create URLs automatically simply by specifying the routing arguments. For example if I want the URL for the page FrontPage I can create it with this code:
1 | h.url_for(title='FrontPage') |
Although the URL would be fairly simple to create manually, with complicated URLs this approach is much quicker. It also has the significant advantage that if you ever deploy your Pylons application at a URL other than /, all the URLs will be automatically adjusted for the new path without you needing to make any manual modifications. This flexibility is a real advantage.
Full information on the powerful things you can do to route requests to controllers and actions can be found in the Routes manual.
8 Controllers
Quick Recap: We've setup the model, configured the application, added the routes and setup the base template in base.mako, now we need to write the application logic and we do this with controllers. In your project's root directory add a controller called page to your project with this command:
1 | $ paster controller page
|
If you are using Subversion, this will automatically be detected and the new controller and tests will be automatically added to your subversion repository.
We are going to need the following actions:
- index(self, title)
- displays a page based on the title
- edit(self, title)
- displays a from for editing the page title
- save(self, title)
- save the page title and show it with a saved message
- list(self)
- gives a list of all pages
- delete(self)
- deletes a page based on an AJAX drag and drop call
Let's get cracking! We just need to make one quick preparation first: edit the BaseController class that your new page controller subclasses, so that we get a clean Session each time one of your controllers is called. Open lib/base.py and edit the __call__ method like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 | from quickwiki.model import Session class BaseController(WSGIController): def __call__(self, environ, start_response): """Invoke the Controller""" # WSGIController.__call__ dispatches to the Controller method the # request is routed to. This routing information is available in # environ['pylons.routes_dict'] try: return WSGIController.__call__(self, environ, start_response) finally: Session.remove() |
This is critical for avoiding unexpected and hard-to-debug behavior resulting from old session data between requests.
8.1 index()
Now we can get to work on the new controller in page.py. First we'll import the Page class from our model class to save some typing later on. Add this line with the imports at the top of the file:
1 | from quickwiki.model import Page |
This is also done the the base.py file for the Session class, as shown above. This is done sheerly for convenience, and you can instead choose to refer to model.Session and model.Page throughout your controllers, since BaseController imports the model for us. This may help to reduce confusion, especially in more complex applications.
On to the index method. Replace the existing index() action with this:
1 2 3 4 5 6 7 8 9 | def index(self, title): page_q = Session.query(Page) page = page_q.filter_by(title=title).first() if page: c.content = page.get_wiki_content() return render('/page.mako') elif model.wikiwords.match(title): return render('/new_page.mako') abort(404) |
Add a template called templates/page.mako that looks like this:
1 2 3 4 | <%inherit file="base.mako"/> <h1 class="main">${c.title}</h1> ${c.content} |
This template simply displays the page title and content.
Note
Pylons automatically assigns all the action parameters to the Pylons context object c so that you don't have to assign them yourself. In this case, the value of title will be automatically assigned to c.title so that it can be used in the templates. We assign c.content manually in the controller.
We also need a template for pages that don't already exist. It needs to display a message and link to the edit action so that they can be created. Add a template called templates/new_page.mako that looks like this:
1 2 3 4 5 6 | <%inherit file="base.mako"/> <h1 class="main">${c.title}</h1> <p>This page doesn't exist yet. <a href="${h.url_for(action='edit', title=c.title)}">Create the page</a>. </p> |
At this point we can test our QuickWiki to see how it looks. If you don't already have a the server running start it now with:
1 | $ paster serve --reload development.ini
|
Visit http://127.0.0.1:5000/ and you will see the front page of the wiki. If you haven't already done so you should delete the file public/index.html so that when you visit the URL above you are routed to the correct action in the page controller and see the wiki front page instead of the index.html file being displayed.
We can spruce it up a little by adding the stylesheet we linked to in the templates/base.mako file earlier. Add the file public/quick.css with the following content and refresh the page to reveal a better looking wiki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | body { background-color: #888; margin: 25px; } div.content{ margin: 0; margin-bottom: 10px; background-color: #d3e0ea; border: 5px solid #333; padding: 5px 25px 25px 25px; } h1.main{ width: 100%; border-bottom: 1px solid #000; } p.footer{ width: 100%; padding-top: 3px; border-top: 1px solid #000; } |
When you run the example you will notice that the word QuickWiki has been turned into a hyperlink by the get_wiki_content() method we added to our Page domain object earlier. You can click the link and will see an example of the new page screen from the new_page.mako template. If you follow the Create the page link you will see the Pylons automatic error handler kick in to tell you Action edit is not implemented. Well, we better write it next, but before we do, have a play with the interactive debugger, try clicking on the + or >> arrows and you will be able to interactively debug your application. It is a tremendously useful tool.
8.2 edit()
To edit the wiki page we need to get the content from the database without changing it to HTML to display it in a simple form for editing. Add the edit() action:
1 2 3 4 5 6 | def edit(self, title): page_q = Session.query(Page) page = page_q.filter_by(title=title).first() if page: c.content = page.content return render('/edit.mako') |
and then create the templates/edit.mako file:
1 2 3 4 5 6 7 8 | <%inherit file="base.mako"/> <h1 class="main">Editing ${c.title}</h1> ${h.start_form(h.url_for(action='save', title=c.title), method="post")} ${h.text_area(name='content', rows=7, cols=40, content=c.content)} <br /> ${h.submit(value="Save changes", name='commit')} ${h.end_form()} |
Note
You might have noticed that we only set c.content if the page exists but that it is accessed in h.text_area even for pages that don't exist and yet it doesn't raise an AttributeError. We are making use of the fact that the c object returns an empty string "" for any attribute that is accessed which doesn't exist. This can be a very useful feature of the c object, but can catch you on occasions where you don't expect this behavior. It can be disabled by setting config['pylons.strict_c'] = True in your project's config/environment.py.
We are making use of the h object to create our form and field objects. This saves a bit of manual HTML writing. The form submits to the save() action to save the new or updated content so let's write that next.
8.3 save()
The first thing the save() action has to do is to see if the page being saved already exists. If not it creates it with page = model.Page(). Next it needs the updated content. In Pylons you can get request parameters from form submissions via GET and POST requests from the appropriately named request.params object. For form submissions from only GET or POST requests, use request.GET or request.POST. Only POST requests should generate side effects (like changing data), so the save action will reference request.POST for the parameters.
Add the save() action:
1 2 3 4 5 6 7 8 9 10 11 12 13 | def save(self, title): page_q = Session.query(Page) page = page_q.filter_by(title=title).first() if not page: page = model.Page() page.title = title page.content = request.POST.get('content','') c.title = page.title c.content = page.get_wiki_content() c.message = 'Successfully saved' Session.save_or_update(page) Session.commit() return render('/page.mako') |
Note
request.params, request.GET and request.POST are MultiDict objects: an ordered dictionary that may contain multiple values for each key. The MultiDict will always return one value for any existing key via the normal dict accessors request.params[key] and request.params.get(key). When multiple values are expected, use the request.params.getall(key) method to return all values in a list.
In order for the page.mako template to display the Successfully saved message after the page is saved we need to update the templates/page.mako file. After <h1 class="main">${c.title}</h1> add these lines:
1 2 3 | % if c.message: <p><div id="message">${c.message}</div></p> % endif |
And add the following to the public/quick.css file:
1 2 3 | div#message{ color: orangered; } |
The % syntax is used for control structures in mako -- conditionals and loops. You must 'close' them with an 'end' tag as shown here. At this point we have a fully functioning wiki that lets you create and edit pages and can be installed and deployed by an end user with just a few simple commands.
Visit http://127.0.0.1:5000 and have a play.
It would be nice to get a title list and to be able to delete pages, so that's what we'll do next!
8.4 list()
Add the list() action:
1 2 3 | def list(self): c.titles = [page.title for page in Session.query(Page).all()] return render('/list.mako') |
The list() action simply gets all the pages from the database. Create the templates/list.mako file to display the list:
1 2 3 4 5 6 7 8 9 10 11 | <%inherit file="base.mako"/> <h1 class="main">Title List</h1> <ul id="titles"> % for title in c.titles: <li> ${title} [${h.link_to('visit', h.url_for(title=title, action="index"))}] </li> % endfor </ul> |
Now we need to edit templates/base.mako to add a link to the title list in the footer, but while we're at it, let's introduce a Mako function to make the footer a little smarter. Edit base.mako like this:
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 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html> <head> <title>QuickWiki</title> ${h.stylesheet_link_tag('/quick.css')} ${h.javascript_include_tag('/javascripts/effects.js', builtins=True)} </head> <body> <div class="content"> ${next.body()}\ <p class="footer"> ${footer(request.environ['pylons.routes_dict']['action'])}\ </p> </div> </body> </html> ## Don't show links that are redundant for particular pages <%def name="footer(action)">\ Return to the ${h.link_to('FrontPage', h.url_for(action="index", title="FrontPage"))} % if action == "list": <% return '' %> % endif % if action != "edit": | ${h.link_to('Edit ' + c.title, h.url_for(title=c.title, action='edit'))} % endif | ${h.link_to('Title List', h.url_for(action='list', title=None))} </%def> |
The <%def name="footer(action"> creates a Mako function for display logic. As you can see, the function builds the HTML for the footer, but doesn't display the 'Edit' link when you're on the 'Title List' page or already on an edit page. It also won't show a 'Title List' link when you're already on that page. The <% ... %> tags shown on the return statement are the final new piece of Mako syntax: they're used much like the ${...} tags, but for arbitrary Python code that does not directly render HTML. Also, the double hash (##) denotes a single-line comment in Mako.
So the footer function is called in place of our old 'static' footer markup. We pass it a value from pylons.routes_dict which holds the name of the action for the current request. The trailing \ character just tells Mako not to render an extra newline.
If you visit http://127.0.0.1:5000/page/list you should see the full titles list and you should be able to visit each page.
8.5 delete()
Since this tutorial is designed to get you familiar with as much of Pylons core functionality as possible we will use some AJAX to allow the user to drag a title from the title list into a trash area that will automatically delete the page.
Add this line to templates/base.mako before </head>:
1 | ${h.javascript_include_tag('/javascripts/effects.js', builtins=True)} |
Note
The h.javascript_include_tag() helper will create links to all the built-in JavaScripts we need and also add /javascripts/effects.js creating HTML that looks like this when you access it from a browser:
1 2 3 | <script src="/javascripts/prototype.js" type="text/javascript"></script> <script src="/javascripts/scriptaculous.js" type="text/javascript"></script> <script src="/javascripts/effects.js" type="text/javascript"></script> |
If you look at config/middleware.py you will see these lines:
1 2 | javascripts_app = StaticJavascripts() app = Cascade([static_app, javascripts_app, app]) |
The javascripts_app WSGI application maps any requests to /javascripts/ straight to the relevant JavaScript in the WebHelpers package. This means you don't have to manually copy the Pylons JavaScript files to your project and that if you upgrade Pylons, you will automatically be using the latest scripts.
Now for the AJAX! We want all the titles in the titles list to be draggable so we enclose each of them with a <span> element with a unique ID. Edit templates/list.mako to look like this:
1 2 3 4 5 6 7 | <%inherit file="base.mako"/> <h1 class="main">Title List</h1> <ul id="titles"> <%include file="list-titles.mako"/> </ul> |
And then create the new templates/list-titles.mako as follows:
1 2 3 4 5 6 7 | % for title in c.titles: <li> <span id="${unicode(title)}">${title}</span> [${h.link_to('visit', h.url_for(title=title, action="index"))}] ${h.draggable_element(unicode(title), revert=True)} </li> % endfor |
Note
You can see that we've moved the for loop into the new template. This is so that we can easily call render() to update it via AJAX from the delete action that we'll add to our controller in just a moment. We <%include /> this new template in the original list.mako; this is a lot like <%inherit />, but moving downward hierarchically instead of upward. It's perhaps the most basic of templating functions and is much like include in PHP templating, for example. Notice that list-titles.mako does not inherit from base.mako like the others we've created. This way we take maximal advantage of Mako's inheritance, while further reducing code duplication with <%include />.
We've also added the <span> tags, and marked each of the titles as a draggable element that reverts to its original position if it isn't dropped over a drop target. If we want to be able to delete the pages we better add a drop target. Try it out at http://127.0.0.1:5000/page/list by dragging the titles themselves around the screen. Notice how much functionality we get with just the one helper h.draggable_element().
We better have somewhere to drop the titles to delete them, so add this before the <ul id="titles"> line in templates/list.mako :
1 2 3 4 | <div id="trash"> Delete a page by dragging its title here </div> ${h.drop_receiving_element("trash", update="titles", url=h.url_for(action="delete"))} |
We will also need to add the style for the trash box to the end of public/quick.css:
1 2 3 4 5 6 7 | div#trash{ float: right; margin: 0px 20px 20px 20px; background: #eee; border: 2px solid #000; padding: 15px; } |
Tip
It can sometimes be very hard to debug AJAX applications. Pylons can help. If an error occurs in debug mode (the default in development.ini) a debug URL where you can use an interactive debugger will be printed to the error stream, even in an AJAX request. If you copy and paste that address into a browser address bar you will be able to debug the request.
When a title is dropped on the trash box an AJAX request will be made to the delete() action, posting an id parameter with the id of the element that was dropped. The element with id titles will be updated with whatever is returned from the action, so we better add a delete() action that returns the new list of titles excluding the one that has been deleted:
1 2 3 4 5 6 7 8 | def delete(self): page_q = Session.query(Page) title = request.POST['id'] page = page_q.filter_by(title=title).one() Session.delete(page) Session.commit() c.titles = page_q.all() return render('/list-titles.mako') |
The title of the page is obtained from the id element and the object is loaded and then deleted. The change is saved with model.Session.commit() before the list of remaining titles is re-rendered by the template templates/list-titles.mako.
Visit http://127.0.0.1:5000/page/list and have a go at deleting some pages. You may need to go back to the FrontPage and create some more if you get carried away!
That's it! A working, production-ready wiki in 20 mins. You can visit http://127.0.0.1:5000/ once more to admire your work.
9 Publishing the Finished Product
After all that hard work it would be good to distribute the finished package wouldn't it? Luckily this is really easy in Pylons too. In the project root directory run this command:
1 | $ python setup.py bdist_egg
|
This will create an egg file in dist which contains everything anyone needs to run your program. They can install it with:
1 | $ easy_install QuickWiki-0.1.5-py2.5.egg
|
You should probably make eggs for each version of Python your users might require by running the above commands with both Python 2.4 and 2.5 to create both versions of the eggs.
If you want to register your project with the Cheeseshop at http://www.python.org/pypi you can run the command below. Please only do this with your own projects though because QuickWiki has already been registered!
1 | $ python setup.py register
|
Warning
The CheeseShop authentication is very weak and passwords are transmitted in plain text. Don't use any sign in details that you use for important applications as they could be easily intercepted.
You will be asked a number of questions and then the information you entered in setup.py will be used as a basis for the page that is created.
Now visit http://www.python.org/pypi to see the new index with your new package listed.
Note
A CheeseShop Tutorial has been written and full documentation on setup.py is available from the Python website. You can even use reStructuredText in the description and long_description areas of setup.py to add formatting to the pages produced on the CheeseShop. There is also another tutorial here.
Finally you can sign in to the CheeseShop with the account details you used when you registered your application and upload the eggs you've created. If that seems too difficult you can even use this command which should be run for each version of Python supported to upload the eggs for you:
1 | $ python setup.py bdist_egg upload
|
Before this will work you will need to create a .pypirc file in your home directory containing your username and password so that the upload command knows who to sign in as. It should look similar to this:
1 2 3 | [server-login] username: james password: password |
Tip
This works on windows too but you will need to set your HOME environment variable first. If your home directory is C:\Documents and Settings\James you would put your .pypirc file in that directory and set your HOME environment variable with this command:
1 | > SET HOME=C:\Documents and Settings\James |
You can now use the python setup.py bdist_egg upload as normal.
Now that the application is on CheeseShop anyone can install it with the easy_install command exactly as we did right at the very start of this tutorial.
10 Security
A final word about security.
!DANGER!
Always set debug = false in configuration files for production sites and make sure your users do to.
You should NEVER run a production site accessible to the public with debug mode on. If there was a problem with your application and an interactive error page was shown, the visitor would be able to run any Python commands they liked in the same way you can when you are debugging. This would obviously allow them to do all sorts of malicious things so it is very important you turn off interactive debugging for production sites by setting debug = false in configuration files and also that you make users of your software do the same.
11 Summary
We've gone through the whole cycle of creating and distributing a Pylons application looking at setup and configuration, routing, models, controllers and templates. Hopefully you have an idea of how powerful Pylons is and, once you get used to the concepts introduced in this tutorial, how easy it is to create sophisticated, distributable applications with Pylons.
That's it, I hope you found the tutorial useful. You are encouraged to email any comments to the Pylons mailing list where they will be gratefully received.
12 ToDo
- If QuickWiki is intended as a reference app for Pylons best practices, I'd like to incorporate some testing into the tutorial. Possibly introduce paster shell too.
- Introduce 0.9.6's logging features instead of sqlalchemy.echo
- Further explain Pylons' Unicode support
13 Thanks
A big thanks to Ches Martin for updating this document and the QuickWiki project for Pylons 0.9.6/QuickWiki 0.1.5, and others in the Pylons community who contributed bug fixes and suggestions.
Comments (39)
Aug 29, 2007
김병건 says:
I failed an section5 when tryed command "paster setup-app development.ini". I us...I failed an section5 when tryed command "paster setup-app development.ini".
I using OS Windows XP and Linux Fedora Core6. but that problem is always existed..
please help me..
error message
-----------------
[root@/home/geon/QuickWiki]#paster setup-app development.ini
Traceback (most recent call last):
File "/usr/local/bin/paster", line 8, in <module>
load_entry_point('PasteScript==1.3.6dev-r6893', 'console_scripts', 'paster')()
File "/usr/local/lib/python2.5/site-packages/PasteScript-1.3.6dev_r6893-py2.5.egg/paste/script/command.py", line 78, in run
invoke(command, command_name, options, args[1:])
File "/usr/local/lib/python2.5/site-packages/PasteScript-1.3.6dev_r6893-py2.5.egg/paste/script/command.py", line 117, in invoke
exit_code = runner.run(args)
File "/usr/local/lib/python2.5/site-packages/PasteScript-1.3.6dev_r6893-py2.5.egg/paste/script/appinstall.py", line 68, in run
return super(AbstractInstallCommand, self).run(new_args)
File "/usr/local/lib/python2.5/site-packages/PasteScript-1.3.6dev_r6893-py2.5.egg/paste/script/command.py", line 212, in run
result = self.command()
File "/usr/local/lib/python2.5/site-packages/PasteScript-1.3.6dev_r6893-py2.5.egg/paste/script/appinstall.py", line 447, in command
conf = appconfig(config_spec, relative_to=os.getcwd())
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 204, in appconfig
global_conf=global_conf)
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 237, in loadcontext
global_conf=global_conf)
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 267, in _loadconfig
return loader.get_context(object_type, name, global_conf)
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 393, in get_context
section)
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 415, in _context_from_use
object_type, name=use, global_conf=global_conf)
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 345, in get_context
global_conf=global_conf)
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 237, in loadcontext
global_conf=global_conf)
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 274, in _loadegg
return loader.get_context(object_type, name, global_conf)
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 542, in get_context
object_type, name=name)
File "/usr/local/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/loadwsgi.py", line 568, in find_egg_entry_point
possible.append((entry.load(), protocol, entry.name))
File "/usr/local/lib/python2.5/site-packages/setuptools-0.6c6-py2.5.egg/pkg_resources.py", line 1912, in load
File "/home/geon/QuickWiki/quickwiki/config/middleware.py", line 13, in <module>
from quickwiki.config.environment import load_environment
File "/home/geon/QuickWiki/quickwiki/config/environment.py", line 34, in <module>
config['pylons.g'].sa_engine = engine_from_config(config, 'sqlalchemy.default.')
File "build/bdist.linux-i686/egg/sqlalchemy/engine/_init_.py", line 178, in engine_from_config
KeyError: 'pop(): dictionary is empty'
Aug 31, 2007
Kailoa Kadano says:
did you install sqlalchemy, sqlite and pysqlite? try these commands... cur...did you install sqlalchemy, sqlite and pysqlite?
try these commands...
curl -O http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.4.0beta4.tar.gz
sudo easy_install SQLAlchemy-0.4.0beta4.tar.gz
rm SQLAlchemy-0.4.0beta4.tar.gz
sudo easy_install pysqlite
Aug 31, 2007
Ches Martin says:
.syntax .c { color: #008800; font-style: italic } /* Comment */ .syntax .err { ...Looks like your database config isn't being found. Are you sure you put this line in the [app:main] section of your development.ini?
If it's there, maybe you don't have SQLite installed (the app itself, not just the Python driver)?
Sep 11, 2007
Randy Pearson says:
I just received the same error at the same point in the tutorial as reported abo...I just received the same error at the same point in the tutorial as reported above:
KeyError: 'pop(): dictionary is empty'
I had previously installed everything and successfully completed the "Starting at the End" portion, even confirming the DB got created, the Wiki would run, etc. Then upon following all the development steps and running
paster setup-app development.ini
I get this error. I have confirmed the correct line is in [app:main]. This is Windows XP Pro. I just rebooted, but same problem. Python25 and Python25\scripts are in my path, and all other tutorial steps up to this point worked fine.
Any help appreciated.
Sep 11, 2007
Randy Pearson says:
I found the problem, maybe it will help the other poster. Back in the instructio...I found the problem, maybe it will help the other poster. Back in the instructions for environment.py, you are to add a final line to the file (config['pylons.g'].sa_engine ...). I wasn't paying attention to what was in that file, and failed to indent this line to match the previous indentation in def load_environment. Complicating that, I was using UltraEdit instead of a Python editor, so didn't have anyone looking over my shoulder or get a compile error. All is well now.
Oct 26, 2007
David Darais says:
I had this exact problem too and you hit the solution right on the head. Althou...I had this exact problem too and you hit the solution right on the head. Although I must say they should add in the wiki that the line you enter at the end of the file must match the indentation of the other statements nested in the def. I ended up rereading that small section a few times and the way its worded for some reason made me think this not to be the case. Looking at in in context, however, i'm a little miffed at myself for not catching it.
Jan 10, 2008
John Harrison says:
It should be noted that there is a bit of confusion because that line is there b...It should be noted that there is a bit of confusion because that line is there by default, but it is also commented out by default. I'd edit the tutorial to make this more clear but I don't seem to have permission to do so. In my case I was getting that error because I glanced at test.ini and thought it was just fine as is rather than removing the #.
Sep 01, 2007
Zangief says:
Having an issue getting started with QuickWiki. I've been able to complete the f...Having an issue getting started with QuickWiki. I've been able to complete the flickr + pylons getting started tutorial, but I can't get to the very first page of QuickWiki [full traceback below].
I tried to install the quickwiki egg as in step #2 before getting started with step #3, and I couldn't get that off the ground either [I used version #10 of the tutorial + easy_install, and ran into db errors] so when I goto localhost:5000, pylons is attempting to look at the [mangled] installation of QuickWiki-0.1.4-py2.5.egg. Any suggestions?
Here's the full traceback:
URL: http://127.0.0.1:5000/
File '/usr/lib/python2.5/site-packages/Pylons-0.9.6rc2-py2.5.egg/pylons/error.py', line 245 in respond
app_iter = self.application(environ, detect_start_response)
File '/usr/lib/python2.5/site-packages/Paste-1.4-py2.5.egg/paste/httpexceptions.py', line 633 in _call_
self.send_http_response, catch=HTTPException)
File '/usr/lib/python2.5/site-packages/Paste-1.4-py2.5.egg/paste/wsgilib.py', line 225 in catch_errors_app
app_iter = application(environ, start_response)
File '/usr/lib/python2.5/site-packages/PasteDeploy-1.3.1-py2.5.egg/paste/deploy/config.py', line 164 in _call_
app_iter = self.application(environ, start_response)
File '/usr/lib/python2.5/site-packages/Pylons-0.9.6rc2-py2.5.egg/pylons/wsgiapp.py', line 291 in _call_
return self.app(environ, start_response)
File '/usr/lib/python2.5/site-packages/Beaker-0.7.5-py2.5.egg/beaker/cache.py', line 180 in _call_
return self.app(environ, start_response)
File '/usr/lib/python2.5/site-packages/Beaker-0.7.5-py2.5.egg/beaker/session.py', line 405 in _call_
response = self.wrap_app(environ, session_start_response)
File '/usr/lib/python2.5/site-packages/Routes-1.7-py2.5.egg/routes/middleware.py', line 104 in _call_
response = self.app(environ, start_response)
File '/usr/lib/python2.5/site-packages/Pylons-0.9.6rc2-py2.5.egg/pylons/wsgiapp.py', line 88 in _call_
response = self.dispatch(controller, environ, start_response)
File '/usr/lib/python2.5/site-packages/Pylons-0.9.6rc2-py2.5.egg/pylons/wsgiapp.py', line 214 in dispatch
return controller(environ, start_response)
File '/usr/lib/python2.5/site-packages/QuickWiki-0.1.4-py2.5.egg/quickwiki/lib/base.py', line 15 in _call_
return WSGIController._call_(self, environ, start_response)
File '/usr/lib/python2.5/site-packages/Pylons-0.9.6rc2-py2.5.egg/pylons/controllers.py', line 208 in _call_
response = self.inspect_call(self.before_)
File '/usr/lib/python2.5/site-packages/Pylons-0.9.6rc2-py2.5.egg/pylons/controllers.py', line 133 in _inspect_call
result = func(**args)
File '/usr/lib/python2.5/site-packages/QuickWiki-0.1.4-py2.5.egg/quickwiki/controllers/page.py', line 7 in _before_
model.ctx.current = make_session()
File '/usr/lib/python2.5/site-packages/Pylons-0.9.6rc2-py2.5.egg/pylons/database.py', line 133 in make_session
engine = create_engine(uri, echo=echo, **kwargs)
File '/usr/lib/python2.5/site-packages/Pylons-0.9.6rc2-py2.5.egg/pylons/database.py', line 80 in create_engine
assert uri
AssertionError:
Sep 08, 2007
Philip Jenvey says:
Zangief - I believe you're using the older QuickWiki (0.1.4) with maybe the newe...Zangief - I believe you're using the older QuickWiki (0.1.4) with maybe the newer development.ini. QuickWiki 0.1.5 isn't up on cheeseshop yet, but will be as soon as Pylons 0.9.6 final is released
Sep 11, 2007
Paul Cook says:
Perhaps under the Templates section the template should be changed to remove the...Perhaps under the Templates section the template should be changed to remove the reference to the footer function that appears in the base.mako template. Since this isn't added until later in the tutorial (the footer function) it produces an error that might be hard to resolve for new users of pylons and or python.
Sep 12, 2007
Ches Martin says:
As it should be, and now it is Thanks for pointing that out!As it should be, and now it is
Thanks for pointing that out!
Sep 12, 2007
Rubin Simons says:
Not sure if this is the right way to do it, but in step 4 (model) is defined lik...Not sure if this is the right way to do it, but in step 4 (model) is defined like
pages_table = Table('pages', metadata,
Column('title', types.String(40), primary_key=True),
Column('content', types.String(), default='')
)
The "types.String" stuff is broken when you want to be able to edit previously inserted utf-8 strings. This fixed it for me (thanks steg!):
pages_table = Table('pages', metadata,
Column('title', types.Unicode(40), primary_key=True),
Column('content', types.Unicode(), default='')
)
Oct 19, 2007
Ches Martin says:
This and a few other fixes have been integrated to make QuickWiki fully Unicode-...This and a few other fixes have been integrated to make QuickWiki fully Unicode-friendly. Thanks to Yannick Gingras and Philip Jenvey for updating the source and the tutorial.
Nov 06, 2007
Tom says:
I'm getting an error when I try to run 'paster setup-app development.ini' at the...I'm getting an error when I try to run 'paster setup-app development.ini' at the end of section 5.
File "/home/tom/QuickWiki/quickwiki/_init_.py", line 13, in <module>
bind=config['pylons.g'].sa_engine))
AttributeError: 'NoneType' object has no attribute 'sa_engine'
Starting at the end as described in section 2 works fine for me. Incidentally, I'm using a mysql database instead of sqlite.
Any suggestions?
Nov 07, 2007
Tom says:
Well, in case anyone else has this problem, I accidentally modified the _init.py...Well, in case anyone else has this problem, I accidentally modified the _init.py_ in QuickWiki/quickwiki instead of the one in QuickWiki/quickwiki/model. As a result middleware.py and environment.py weren't getting read.
Jan 19, 2008
Somanyw Hales says:
Great tutorial. This really shows the power of the Pylons framework and the spee...Great tutorial. This really shows the power of the Pylons framework and the speed at which one can develop some fairly advanced web apps. Thanks to those who put it together.
I have a few suggestions, related to the security and integrity of QuickWiki. Fixing these would, I believe, lend further credibility to the claim at the beginning that this is a production ready web app, while at the same time bringing up some important topics that I haven't seen mentioned too often elsewhere in the Pylons tutorials.
I had originally posted these as a Confluence "Review" to this page, but I didn't see a response.
On to the suggestions:
1. It is possible to create pages that aren't wiki words. For example, loading the page http://127.0.0.1:5000/page/edit/foo will allow one to create the foo page, which doesn't seem to be intended by the app otherwise. Using the 'save' action directly with a non-wikiword as the title accomplishes the same thing.
Possible fix: In the controller's 'edit' and 'save' actions, add the following code:
if model.wikiwords.match(title) is None: abort(404)This is very similar to the check in the index action, and shouldn't require much new explanatory text.
2. The current use of docutils allows one to create pages that include text directly from files in the server's filesystem. This is a fairly major security breach! To test this, if the server is Unix-like, you could, for example, edit a page's content to:
And, when rendered, the page will include the full contents of the system's /etc/fstab file.
This type of vulnerability and its fix are described here:
http://docutils.sourceforge.net/docs/howto/security.html
In short, it is possible to turn off the 'include', 'raw', and similar docutils directives via configuration files or by passing a dictionary to override the defaults, as in:
defaults = {'file_insertion_enabled': 0, 'raw_enabled': 0} output = docutils.core.publish_string(..., settings_overrides=defaults)3. Input is not being validated, resulting in the possibility of unsanitized data being stored in the QuickWiki model. That is, the 'save' action does not escape the content of web pages. Since it uses docutils, the unsafe data is not a danger to QuickWiki users. A problem arises though if same model were ever linked to a different view/controller that wasn't cautious. As I see it, best practices would dictate only storing sanitized data in any model.
Possible fix: Save the result of cgi.escape(page.content) in the model instead of just page.content. That is:
i. add
to the top of the file
ii.
page.content = request.POST.get('content', '')becomes
page.content = escape(request.POST.get('content', ''))This does not affect the result of the publish_parts() call when retrieving and displaying the content.
4. It is possible to cause an error traceback by triggering the 'delete' action with an invalid title in the HTTP POST data. This is caused by use of the POST dictionary directly instead of a get() call (as seen in the 'save' action).
(It also seems likely that one() is not meant in this action. The first() method seems to better match my perception of the desired behaviour.)
Jan 29, 2008
lixum says:
Hi all ! Yes i agree, it definitely shows the power of pylon and python on web ...Hi all !
Yes i agree, it definitely shows the power of pylon and python on web in general.
But I'd really like to see Christopher's suggestions merged into this tutorial. There are a lot of potential programers out there, seeking for alternatives to the (imho) broken PHP. They may become deterred quickly
Also, the fix for the problem described here: http://www.mail-archive.com/pylons-discuss@googlegroups.com/msg06304.html did not found its way into the reposity, yet.
Feb 13, 2008
Kevin Teague says:
I'm a Pylons newbie, but I believe that the Page Model object should probably be...I'm a Pylons newbie, but I believe that the Page Model object should probably be refactored so that the HTML specific formatting happens in the Page View (or perhaps the Page Controller). That is the usage of h.url_for() and h.link_to() helpers and the docutils HTML formatting?
A more minor quibble, but I would also rename the "title" attribute to "name". Title tends to mean "human-readable description of content" while name means "unique human-readable identifier". Titles may not always be unique, and may contain characters which can't be represented as part of an URL. At least that's my CMS'ish definition of the term - these are somewhat fuzzy words with many interpretations.
Feb 13, 2008
Jorge Vargas says:
Did you miss the part where it says why it's all done in the model? in this part...Did you miss the part where it says why it's all done in the model? in this particular case the HTML is the data, therefore it's ok to manipulate it at the model layer. On the other hand if they implement your suggestion you will have to do the html transformation on all views which is redundant.
As for your second suggestion it's very semantical. Another way of looking at it is that the title is the attribute you show on the status bar, but considering wiki's come from publishing sites title in this case does refers to what it's supposed to be. either way it's just an example
Feb 13, 2008
Jorge Vargas says:
Given that I couldn't find the edit link I assume it's reserved to prevent spam,...Given that I couldn't find the edit link I assume it's reserved to prevent spam, therefore here is my correction.
When the tutorial was migrated to Unicode whoever did it forgot to update the values in quickwiki/websetup.py which is generating some warnings on the sqlalchemy layer about inserting strings into unicode fields.
therefore lines 27-28 should be
page.title=u'FrontPage'
page.content=u'Welcome to the QuickWiki front page'
Feb 13, 2008
Jorge Vargas says:
FOr some reason, the sqlalchemy.default.echo = True is outputting the commands t...FOr some reason, the sqlalchemy.default.echo = True is outputting the commands twice, could this be something left by by way way back when SA didn't used the logging module?
Feb 13, 2008
Jorge Vargas says:
some more stuff • typo at The <%def name="footer(action"> creates • in ...some more stuff
• typo at The <%def name="footer(action"> creates
• in seccion6 the javascript line is added to the templates but then on 8.5 there is no note saying it should already be there.
Feb 18, 2008
Gavin Engel says:
On Section 5: I type in console: /var/www/QuickWiki$ sudo paster setup-app devel...On Section 5:
I type in console:
/var/www/QuickWiki$ sudo paster setup-app development.ini
And I get this:
/var/lib/python-support/python2.5/kid/pull.py:16: DeprecationWarning: kid.pull has been superseded by kid.parser
warnings.warn("kid.pull has been superseded by kid.parser", DeprecationWarning)
Running setup_config() from quickwiki.websetup
Traceback (most recent call last):
File "/usr/bin/paster", line 8, in <module>
load_entry_point('PasteScript==1.3.6', 'console_scripts', 'paster')()
File "/usr/lib/python2.5/site-packages/PasteScript-1.3.6-py2.5.egg/paste/script/command.py", line 78, in run
invoke(command, command_name, options, args[1:])
File "/usr/lib/python2.5/site-packages/PasteScript-1.3.6-py2.5.egg/paste/script/command.py", line 117, in invoke
exit_code = runner.run(args)
File "/usr/lib/python2.5/site-packages/PasteScript-1.3.6-py2.5.egg/paste/script/appinstall.py", line 68, in run
return super(AbstractInstallCommand, self).run(new_args)
File "/usr/lib/python2.5/site-packages/PasteScript-1.3.6-py2.5.egg/paste/script/command.py", line 212, in run
result = self.command()
File "/usr/lib/python2.5/site-packages/PasteScript-1.3.6-py2.5.egg/paste/script/appinstall.py", line 456, in command
self, config_file, section, self.sysconfig_install_vars(installer))
File "/usr/lib/python2.5/site-packages/PasteScript-1.3.6-py2.5.egg/paste/script/appinstall.py", line 592, in setup_config
mod.setup_config(command, filename, section, vars)
File "/var/www/QuickWiki/quickwiki/websetup.py", line 14, in setup_config
load_environment(conf.global_conf, conf.local_conf)
File "/var/www/QuickWiki/quickwiki/config/environment.py", line 34, in load_environment
config['pylons.g'].sa_engine = engine_from_config(config, 'sqlalchemy.default.')
NameError: global name 'engine_from_config' is not defined
Any help would be appreciated.
Feb 18, 2008
Jorge Vargas says:
Did you miss the line where you import that? from sqlalchemy import engine_from...Did you miss the line where you import that?
from sqlalchemy import engine_from_config
Feb 18, 2008
Gavin Engel says:
That is exactly what I did wrong! Stupid me, and thanks for saving me a headach...That is exactly what I did wrong! Stupid me, and thanks for saving me a headache.
Feb 20, 2008
Gavin Engel says:
A tiny suggestion. As with many software tutorials, oft times the student will ...A tiny suggestion.
As with many software tutorials, oft times the student will accidentally skip one of these important step. This causes the house of cards to crash down and can be really frustrating. My suggestion, at every major step along the way have a *.zip file of the files as they should look.
Mar 28, 2008
Aleksey Naumov says:
Thank you for nice tutorial. I just downloaded QuickWiki 0.15 and was testing it...Thank you for nice tutorial. I just downloaded QuickWiki 0.15 and was testing it out following section "Starting at the End". However, it seems I am not able to edit pages. I created the QuickWiki page, but when I attempt to edit that (or the FrontPage) I get an error about page being already persistent. Does this have something to do with session management? What am I doing wrong and how can I fix that?
Thank you very much
Aleksey
Here is a traceback:
Module quickwiki.controllers.page:37 in save
<< c.content = page.get_wiki_content()
c.message = 'Successfully saved'
Session.save(page)
Session.commit()
return render('/page.mako')>> Session.save(page)
Module sqlalchemy.orm.scoping:98 in do
<< def instrument(name):
def do(self, *args, **kwargs):
return getattr(self.registry(), name)(*args, **kwargs)
return do
for meth in ('get', 'load', 'close', 'save', 'commit', 'update', 'save_or_update', 'flush', 'query', 'delete', 'merge', 'clear', 'refresh', 'expire', 'expunge', 'rollback', 'begin', 'begin_nested', 'connection', 'execute', 'scalar', 'get_bind', 'is_modified', '_contains', 'iter_'):>> return getattr(self.registry(), name)(*args, **kwargs)
Module sqlalchemy.orm.session:893 in save
<< specific ``Mapper`` used to handle this instance.
"""
self._save_impl(instance, entity_name=entity_name)
self._cascade_save_or_update(instance)>> self._save_impl(instance, entity_name=entity_name)
Module sqlalchemy.orm.session:1062 in _save_impl
<< def _save_impl(self, instance, **kwargs):
if hasattr(instance, '_instance_key'):
raise exceptions.InvalidRequestError("Instance '%s' is already persistent" % mapperutil.instance_str(instance))
else:
>> raise exceptions.InvalidRequestError("Instance '%s' is already persistent" % mapperutil.instance_str(instance))
sqlalchemy.exceptions.InvalidRequestError: Instance 'Page@0x152ad90' is already persistent
Mar 31, 2008
Wojciech Malinowski says:
It is caused by a change of SQLAlchemy behavior in version 0.4.1 and above. You ...It is caused by a change of SQLAlchemy behavior in version 0.4.1 and above. You can simply modify one line of QuickWiki code as here: http://www.knowledgetap.com/hg/QuickWiki/rev/627982b1db52
Apr 07, 2008
Aleksey Naumov says:
Thank you, Wojciech, it worked.Thank you, Wojciech, it worked.
Apr 23, 2008
Joseph Rawson says:
in the delete() method of the Page controller, I had to set c.titles = [pag...in the delete() method of the Page controller, I had to set
c.titles = [page.title for page in page_q.all()]
else there would be no titles displayed after a successful drag to the trash box.
May 26, 2008
Patrick G. Hampton says:
I have followed the tutorial, but have a problem with creating new pages. Lookin...I have followed the tutorial, but have a problem with creating new pages. Looking at the html source from the browser, I see the QuickWiki url is "QuickWiki?id=None". And when I try to create the page is is trying to go to "http://127.0.0.1/None" which just gives me a 404 error. What and where should I look for the problem. I have check all the changed made to the quickwiki, but I have not seen anything there that would be causing the improper url.
May 28, 2008
Patrick G. Hampton says:
Nevermind, found the problem in the routing.py. Typoed the ':controller/:action/...Nevermind, found the problem in the routing.py. Typoed the ':controller/:action/:title' route with ':controller/:action/:id'. Don't know why, but I did.
Jul 14, 2008
Rian Hunter says:
This tutorial only works if you want to run a QuickWiki on the SQLite engine. If...This tutorial only works if you want to run a QuickWiki on the SQLite engine. If you want to use any other database, QuickWiki is broken. The Page model uses a length-less String() type which only works on SQLite. I tried finding the emails of the people who maintain QuickWiki but couldn't. They should change it and give it a length.
Jul 17, 2008
Rémy Roy says:
Hello, This is a great tutorial. Thank you. I did it with SQLAlchemy 0.5beta1 ...Hello,
This is a great tutorial. Thank you.
I did it with SQLAlchemy 0.5beta1 because it is the one that I got from easy_install by default. Here are a few warning that popped up while I did the tutorial.
In your model/_init_.py , on line starting with "Session = scoped_session(sessionmaker(", transactional=True is no more used. You might want to remove it.
In your websetup.py , while inserting the default FrontPage, you might want to use unicode strings since you told SQLAlchemy that your model is using Unicode field type. You might want to add "u" before the string in those statements:
page.title = u'FrontPage'
page.content = u'Welcome to the QuickWiki front page.'
Thanks again.
Nov 05, 2008
Michael Miller says:
I got stuck in two ways, first I wanted to use MySQL so I added sqlalchemy.defa...I got stuck in two ways, first I wanted to use MySQL so I added
sqlalchemy.default.url = mysql://localhost:8889/quickwiki
to the test.ini file
this failed because permissions were not set correctly. I guess I need to look more closely at the SQLAlchemy instructions.
then I tried to install sqlite according to the first comment. The install went fine but when I went to
paster setup-app test.ini
I got the following:
<snip>
def find_plugins(self,
pkg_resources.VersionConflict: (SQLAlchemy 0.4.0beta4 (/Library/Python/2.5/site-packages/SQLAlchemy-0.4.0beta4-py2.5.egg), Requirement.parse('SQLAlchemy>=0.4.0beta5'))
So I thought I'd try an upgrade:
captive-wireless-250-249:controllers mimiller$ sudo easy_install SQLAlchemy-0.4.0beta5.tar.gz
Searching for SQLAlchemy-0.4.0beta5.tar.gz
Reading http://pypi.python.org/simple/SQLAlchemy-0.4.0beta5.tar.gz/
Couldn't find index page for 'SQLAlchemy-0.4.0beta5.tar.gz' (maybe misspelled?)
Scanning index of all packages (this may take a while)
Reading http://pypi.python.org/simple/
No local packages or download links found for SQLAlchemy-0.4.0beta5.tar.gz
error: Could not find suitable distribution for Requirement.parse('SQLAlchemy-0.4.0beta5.tar.gz')
any suggestions?
Nov 07, 2008
Ches Martin says:
Hi Michael, Pylons 0.9.7 was tagged for release a few hours ago, and SQLAlchemy...Hi Michael,
Pylons 0.9.7 was tagged for release a few hours ago, and SQLAlchemy is in a release candidate stage for version 0.5. The QuickWiki tutorial now sorely needs to be updated and may be difficult to follow until then, but it may not happen until the dust settles a bit.
If you're just dipping your feet into Pylons, I'd suggest checking out http://pylonsbook.com/, as it (along with http://docs.pylonshq.com/) will probably be the most up-to-date source of information until official tutorials are brought back up to snuff. Since the Sphinx documentation system has come to be in the long months leading to 0.9.7, docs have been a heavy focus for the release, and a new Pylons site is forthcoming, I suspect that the sources of documentation will soon be less confusing.
Good luck!
Nov 26, 2008
Adam Sloan says:
Working with Pylons 0.9.6.2 and Python25, did the "Installing Pylons" and the "G...Working with Pylons 0.9.6.2 and Python25, did the "Installing Pylons" and the "Getting Started", then the top of this page (Starting from End), which is 4 commands.
Got this warning:
C:\Python25\pylons>paster setup-app test.ini
Running setup_config() from quickwiki.websetup
C:\Python25\lib\site-packages\quickwiki-0.1.5-py2.5.egg\quickwiki\model_init_.py:15: SADeprecati
nWarning: The 'transactional' argument to sessionmaker() is deprecated; use autocommit=True|False i
stead.
bind=config['pylons.g'].sa_engine))
Fired it up and everything seemed ok, could click on links, but then tried to edit the Front Page and it choked (I don't want to fill this page with long error messages, ):
C:\Python25\pylons>paster serve test.ini
Starting server in PID 336.
serving on 0.0.0.0:5000 view at http://127.0.0.1:5000
Error - <class 'sqlalchemy.exc.InvalidRequestError'>: Instance '<Page at 0x3174b90>' is already pers
istent
URL: http://localhost:5000/page/save?content=Welcome+to+the+QuickWiki+front+page.%0D%0Ablah&commit=S
ave+changes
Mar 03, 2009
Bayle Shanks says:
dead link: http://pylonshq.com/WebHelpers/module-index.htmldead link: http://pylonshq.com/WebHelpers/module-index.html
Mar 17
Cory Engebretson says:
Following the Quickwiki tutorial, I discovered that QuickWiki-0.1.6 didn't work ...Following the Quickwiki tutorial, I discovered that QuickWiki-0.1.6 didn't work due to changes in WebHelpers. From pypi, I got the latest WebHelpers (1.0.4?), but had to downgrade to 0.6.4 to get it to work.
easy_install -U WebHelpers==0.6.4