The World Through the Eyes of John Brennan
I’ll assume you’ve heard the buzz about Ruby on Rails. Pylons, like Rails, is an MVC framework, but written in the Python language.
This isn’t meant to be an intro to MVC frameworks, but for those just getting started… MVC frameworks let you keep the presentation, business logic, and data separate. They also take care of the majority of setup work involved with building a new web app. I could write another post just on this topic alone, but many people already have. BetterExplained as a great tutorial entitled, Understanding Models, Views and Controllers.
I’ve been using Pylons for about 9 months now. I have picked up some nice conventions, and created some of my own along the way. I had initially developed a web app with Pylons 0.9.5, and it was quite impressive. When learning all new frameworks there is a bit of a learning curve, especially when slapping together several pieces of middleware and other various tools.
Last week I upgraded to Pylons 0.9.6 — and things broke. Although it didn’t take nearly as much time to get moving again, there were definitely some hiccups.
In the hopes of alleviating your pain, I will show you how to get Pylons working with an authentication system (AuthKit), the database model (SQLAlchemy 0.4), and some conventions I established with the Mako templating engine.
This tutorial assumes that you already have Python 2.x and Pylons installed, and that you have created your first basic project. If not, don’t worry! Just check out the great tutorial put together by the community on Getting started with Pylons.
Ok… now that we are on the same page, you’ll need to grab 1 or 2 more packages before we continue. After that I will walk you through each file you’ll need to edit.
Use easy install for quick installation of these kits or see Installing Pylons to get easy install working for you. If you just installed Pylons for the first time you’ll probably already have the latest version of SQLAlchemy. This tutorial was written for the versions specified below, so later versions may not always work the same way. If you find a later version doesn’t quite work, please post a comment (with the solution changes if possible). Thanks!
easy_install SQLAlchemy==0.4 easy_install AuthKit==0.3.0
Note: Replace all occurrences of MYFIRSTAPP with the name of your application.
"""Pylons environment configuration""" import os from pylons import config from sqlalchemy import engine_from_config from MYFIRSTAPP.model import init_model import MYFIRSTAPP.lib.app_globals as app_globals import MYFIRSTAPP.lib.helpers from MYFIRSTAPP.config.routing import make_map def load_environment(global_conf, app_conf): """Configure the Pylons environment via the ``pylons.config`` object """ print "Loading environment..." # Pylons paths root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) paths = dict(root=root, controllers=os.path.join(root, 'controllers'), static_files=os.path.join(root, 'public'), templates=[os.path.join(root, 'templates')]) # Initialize config with the basic options config.init_app(global_conf, app_conf, package='MYFIRSTAPP', template_engine='mako', paths=paths) config['routes.map'] = make_map() config['pylons.g'] = app_globals.Globals(global_conf, app_conf) config['pylons.h'] = MYFIRSTAPP.lib.helpers # Customize templating options via this variable tmpl_options = config['buffet.template_options'] # # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) # tmpl_options['mako.imports'] = ['from webhelpers import auto_link as l', 'from webhelpers import simple_format as s', 'from MYFIRSTAPP.lib import myfilters', 'from pylons import config as CONFIG'] # Give support for Unicode in Mako templating system # from: http://wiki.pylonshq.com/pages/viewpage.action?pageId=5439551 tmpl_options['mako.input_encoding'] = 'utf-8' tmpl_options['mako.output_encoding'] = 'utf-8' # Load Database Model print "Loading database model from <sqlalchemy.default> config..." engine = engine_from_config(config, 'sqlalchemy.default.') init_model(engine)
You’ll notice that I am importing several files to use in Mako, my templating engine of choice. I want to make my config file (development.ini or whichever other config file I decide to load my app with) available to the templates. I also import a file I created called myfilters. This gives me the ability to use additional filters that might not be included with WebHelpers (a wonderful library that gets included with Pylons out of the box). I will show you what to do with this file once we take a look at the files in the /lib/ directory.
"""Pylons middleware initialization""" from paste.cascade import Cascade from paste.registry import RegistryManager from paste.urlparser import StaticURLParser from paste.deploy.converters import asbool from pylons import config from pylons.error import error_template from pylons.middleware import error_mapper, ErrorDocuments, ErrorHandler, \ StaticJavascripts from pylons.wsgiapp import PylonsApp from MYFIRSTAPP.config.environment import load_environment def make_app(global_conf, full_stack=True, **app_conf): # Configure the Pylons environment load_environment(global_conf, app_conf) # The Pylons WSGI app app = PylonsApp() # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, error_template=error_template, **config['pylons.errorware']) # import AuthKit import authkit.authenticate app = authkit.authenticate.middleware(app, app_conf) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) app = ErrorDocuments(app, global_conf, mapper=error_mapper, **app_conf) # Establish the Registry for this application app = RegistryManager(app) # Static files javascripts_app = StaticJavascripts() static_app = StaticURLParser(config['pylons.paths']['static_files']) app = Cascade([static_app, javascripts_app, app]) return app
Nothing to interesting here. This is where we are adding the AuthKit to our middleware stack.
Let’s turn our attention over to the /lib folder of the project.
All we’re adding here is a line to import the model.
from MYFIRSTAPP.model import init_model class Globals(object): def __init__(self, global_conf, app_conf, **extra): """ Globals acts as a container for objects available throughout the life of the application. One instance of Globals is created by Pylons during application initialization and is available during requests via the 'g' variable. ``global_conf`` The same variable used throughout ``config/middleware.py`` namely, the variables from the ``[DEFAULT]`` section of the configuration file. ``app_conf`` The same ``kw`` dictionary used throughout ``config/middleware.py`` namely, the variables from the section in the config file for your application. ``extra`` The configuration returned from ``load_config`` in ``config/middleware.py`` which may be of use in the setup of your global variables. """ pass
Base is called before entering a controller. AuthKit needs to be integrated into this file as well. The database session also gets initialized at this point. You can take the approach of using AuthKit on a case by case basis, but my app requires the user to be logged in for all tasks.
import md5 from pylons import c, config as CONFIG, g, cache, request, response, session from pylons.controllers import WSGIController from pylons.decorators import jsonify, validate from pylons.templating import render, render from pylons.controllers.util import abort, redirect_to, etag_cache from pylons.i18n import N_, _, ungettext from sqlalchemy import * from authkit.permissions import NotAuthenticatedError from MYFIRSTAPP.model import meta from MYFIRSTAPP.model.tables import * import MYFIRSTAPP.lib.helpers as h class BaseController(WSGIController): def __call__(self, environ, start_response): # Insert any code to be run per request here. The Routes match # is under environ['pylons.routes_dict'] should you want to check # the action or route vars here # Each request explicitly gets its own session conn = meta.engine.connect() meta.Session.configure(bind=conn) try: return WSGIController.__call__(self, environ, start_response) finally: meta.Session.remove() conn.close() def __before__(self, action, **params): # Init variables for template c.auth_user = {'id':0, 'nickname': '', 'first': '', 'signed_in': False} # add actions here that do NOT require auth if action == 'do_signin' or action == 'login_screen' or action == 'do_signout': return elif not request.environ.has_key('REMOTE_USER'): # Message to pass to template c.message = "Please sign in" raise NotAuthenticatedError('Not Authenticated') else: user = meta.Session.query(User).filter_by(nickname=request.environ.get('REMOTE_USER')).first() if user: # Variables to make available in all templates (and controllers) c.auth_user = {'id': user.user_id, 'nickname': user.nickname, 'first': user.first_name, 'signed_in': True} def login_screen(self): return render("/home/login.mako") def do_signin(self): if len(request.params) > 1: uname = request.params.get('username', '').strip() pword = request.params.get('password', '').strip() user = meta.Session.query(User).filter_by(nickname=uname).first() entered_password = md5.new(pword) if user and (user.password == entered_password.hexdigest()): request.environ['paste.auth_tkt.set_user'](uname.encode('utf8')) h.redirect_to('home') else: c.message = "Invalid username and/or password" return render('/home/login.mako') else: c.message = "Please enter your username and password" return render('/home/login.mako') def do_signout(self): if not request.environ.has_key('REMOTE_USER'): c.message = "You are not logged in" return render('/home/login.mako') else: c.message = "You have been logged out" return render('/home/login.mako') # Include the '_' function in the public names __all__ = [__name for __name in locals().keys() if not __name.startswith('_') \ or __name == '_']
Here, we are defining the methods that take care of authenticating the user. I will show you the routes I use later to make this possible.
As I mentioned above, I am using some custom filters in my Mako templates that WebHelpers doesn’t provide. Since I included the file above, here is a snippet of what the file looks like. I will be showing you how to use these in your templates later in this Zero to 60 with Pylons series.
""" Template filters All names available in this module will be available in all Mako templates """ import MYFIRSTAPP.lib.helpers as h import cgi import re from datetime import time, datetime from time import strptime # # Right now the in_format and out_format are predefined # We should use the decorator pattern and allow caller to specify in and out formats for datetime object # # see: http://groups.google.com/group/mako-discuss/browse_thread/thread/ef994d9488e8973a/5918bfc84474fafc?lnk=gst&q=filters&rnum=12#5918bfc84474fafc # def transform_date(text): dt = datetime(*strptime(text, "%Y-%m-%d %H:%M:%S")[0:6]) return dt.strftime("%b %d %Y %H:%M:%S %Z") def transform_date_tuple(date_tuple_string): date_tuple_string = date_tuple_string.replace('(','') date_tuple_string = date_tuple_string.replace(')','') date_list = date_tuple_string.split(', ') date_tuple = datetime(int(date_list[0]), int(date_list[1]), int(date_list[2]), int(date_list[3]), int(date_list[4]), int(date_list[5])) return date_tuple.strftime("%b %d %Y %H:%M:%S %Z") # # Converts a datetime object into a string representing time past # Will apply special style to time past if within 24 hours # # Output format: # '4 hours ago' # def str_to_time_ago(dt_str): dt = str_to_datetime(dt_str) # Convert date to something like 'about 4 hours ago' text = h.time_ago_in_words(dt) + ' ago' # if within 24 hours then add class now_time = datetime.now() difference = now_time-dt #if you wanted to get weeks and days #weeks, days = divmod(difference.days, 7) if difference.days < 1: text = '<span class="timestamp_new">' + text + '</span>' return text # # Converts a standard datetime string into a datetime object # Possible input formats: # '2007-09-05 17:10:27' # '2007-09-05' # def str_to_datetime(dt_str): # Parse the string pieces = dt_str.split(' ') # Do we have a date or datetime str? if len(pieces) > 1: mytime = pieces[1].split(':') else: mytime = [0, 0, 0] mydate = pieces[0].split('-') dt = datetime(int(mydate[0]), int(mydate[1]), int(mydate[2]), int(mytime[0]), int(mytime[1]), int(mytime[2])) return dt
This is usually only called when you make a change to the database model (or setting up for the first time). It will tear down your database model and rebuild it. It basically imports your model and calls the create_all method that will build your database model.
"""Setup the MYFIRSTAPP application""" import logging from paste.deploy import appconfig from pylons import config from MYFIRSTAPP.config.environment import load_environment log = logging.getLogger(__name__) def setup_config(command, filename, section, vars): """Place any commands to setup temp here""" conf = appconfig('config:' + filename) load_environment(conf.global_conf, conf.local_conf) # Import late. Otherwise, if anything is wrong in the model, the # whole import will fail, and Paste will mistakenly think that # websetup doesn't even exist. from MYFIRSTAPP.model import create_all print "Creating tables..." create_all(conf.local_conf) print "\n\n-----------------------------------" print " SETUP SUCCESSFUL" print "-----------------------------------"
This is the configuration file that you will load when serving up your web app. I show you development.ini here, but you’ll basically do the same thing for production.ini with the exception of a few tweaks. Tweaks could include setting debug to false, changing variables that affect UI, etc. My reason for including this is to show you where to place the AuthKit and SQLAlchemy settings.
These tweaks must be part of the config for [app:main]. I put them under:
#set debug = falseauthkit.enable = true authkit.method = forward authkit.cookie.secret = a random dtk string authkit.signin = /signin authkit.cookie.signout = /signout #authkit.users.setup = user:pylons # Specify the database for SQLAlchemy to use via # pylons.database.session_context. # %(here) may include a ':' character on Windows environments; this can # invalidate the URI when specifying a SQLite db via path name sqlalchemy.default.url = mysql://USERNAME:PASSWORD@localhost:PORT/MYFIRSTAPP sqlalchemy.default.echo = True sqlalchemy.default.pool_recycle = 14400 sqlalchemy.default.convert_unicode = True sqlalchemy.default.encoding = utf-8 # leave blank or use 'default' if using default seed set database.seed_set = default
I will discuss the database seed file in the next part of this series.
Continue to Part 2 and find out how to modify the model
Feeling anxious? Download the bare bones project now.
Code. Design. Explore. is the blog of John Brennan, a web developer/designer, entrepreneur, and avid world traveler. I currently live in San Diego, CA, USA.
My first passion is to create. I want to be part of a successful startup that will empower others. I believe in designing for the user and appreciate other web apps that design for usability.
My second passion is to help. My heart lies in philanthropy and helping others that are just as able, but haven't been afforded the same opportunities only because they were born at a different coordinate on this Earth.
This blog will mostly be around building cool things, although I will surely include my travel experiences when I am abroad. Feel free to subscribe to a specific category if that is only what interests you. And please connect with me. I always enjoy meeting new, interesting people!
phipster » Blog Archive » Pylons stuff
April 13th, 2008 at 10:36 pm
[…] Brennan brings us a short Pylons intro Zero to 60 with Pylons… in just minutes (Part 1). Can hardly wait for Part […]
thekid
April 30th, 2008 at 3:44 pm
Hi, this is a very interesting tutorial.
I have a few issues and questions, and I will post them in this page and Part 2 page.
Also, here’s my environment:
Python 2.5; Pylons 0.9.6.1; setuptools 0.6c8; PasteScript 1.6.2; SQLAlchemy 0.4.5; AuthKit 0.4.0
I run all these on Windows XP.
Ok, here we go.
1. I had to change MYFIRSTAPP.models to MYFIRSTAPP.model because it’s “model” instead of “models” in my Pylons project directory.
2. [code]app = authkit.authenticate.middleware(app, config_paste=app_conf)[/code]
I changed the second argument to “app_conf”
Please see http://wiki.pylonshq.com/display/pysbook/Authentication+and+Authorization#authentication-middleware
3. I think your development.ini is still using the “old convention” for AuthKit. This is what I have:
authkit.setup.enable = true
authkit.setup.method = forward
authkit.cookie.secret = a random dtk string
authkit.forward.internalpath = /signin
authkit.cookie.signout = /signout
Honestly, I don’t really understand how to use AuthKit, so your help will be appreciated.
4. Last thing for this page, a question:
How do you install Rosetta? Or perhaps it’s not possible at all on a Windows machine?
Thanks.
thekid
April 30th, 2008 at 3:54 pm
My apology for saying that I would post my next question in the Part 2 page. I think my question is more appropriate to be asked here, because I know my Pylons project doesn’t reach the database at all =p
So, after I made changes needed just to squash all compile errors, I have this “Internal Server Error” on the browser page, and in the command prompt it says:
AssertionError: Forwarding loop detected; ‘/signin’ visited twice (internal redirect path: [’/favicon.ico’, ‘/signin’])
Do you have any idea what causes it and how to fix it?
Many thanks for the tutorial.
John Brennan
May 2nd, 2008 at 10:20 am
@thekid:
Glad you like the tutorial. Based on your comments I probably should update the post a bit. Here are some answers to your Qs.
1) you are probably using paster to create the controllers. in 0.9.6 they changed the name, but because I had upgraded from 0.9.5 I had the old folder names still. I’ll update this. Thanks
———-
2) Thanks. I’ll test it and update the post.
———-
3) Again, thanks. What don’t you understand about AuthKit?
Basically the way it works is by authenticating the user before he enters the code requiring auth. I put it in base.py: __before__() because base always gets called before entering the desired controller. Also, the __before__ method tells the program to execute that code before any other functions.
For example, if you were calling method add() in your FooController class, but you also defined a __before__ method in that class, then even though you are making a call to add(), __before__() will execute first.
Does that help?
———-
4) Rosetta was actually the app that I was using. I thought I renamed all instances to MYFIRSTAPP, but I guess not. I will change this. So, when I say “config[’pylons.h’] = rosetta.lib.helpers” I mean “config[’pylons.h’] = MYFIRSTAPP.lib.helpers”
5) [e.g. 2nd post] This may be a problem with Routes. Routes are executed from top to bottom, but as soon as it finds a regex match it will stop further processing and jump into the specified controller. Make sure /favicon.ico is going to the image and not a bogus controller. What is your signin method pointing to? All of my controllers require authentication, so I placed it in base. In this case, even though I said that /signin should go to the non-existant auth controller, it is actually being caught by lib/base.py first, and I’m defining the template there, so it never actually hits the authcontroller.
Does that help?