Many sites have both public and private pages, where anyone can access the public pages, but only logged-in, registered users can access the private pages.
Here's a simple implementation using CherryPy 2.0 and Python 2.4 decorators:
from cherrypy import cpg def html(title): '''HTML decorator: wrap the results of the called function in HTML header and footer tags, setting the page title in the HTML HEADer. Because this decorator takes arguments, it has to return _another_ decorator that actually uses the arguments (see PEP 318 for details). ''' def _wrapper(fn): def _innerWrapper(*args, **kwargs): '''The .join(list)() nonsense is just in case you're using the generator filter (since the filter doesn't get a chance to act inside the decorator.''' return '<HTML><HEAD><TITLE>%s</TITLE></HEAD><BODY>%s</BODY></HTML>' % (title, "".join(list(fn(*args, **kwargs)))) return _innerWrapper return _wrapper def needsLogin(fn): '''Login decorator: if the user is not logged in, send up a login rather than the page content. Point the login form back to the same page to avoid a redirect when the user logs in correctly. ''' def _wrapper(*args, **kwargs): if cpg.request.sessionMap.has_key('userid'): # User is logged in; allow access return fn(*args, **kwargs) else: # User isn't logged in yet. # See if the user just tried to log in try: submit = kwargs['login'] email = kwargs['loginEmail'] password = kwargs['loginPassword'] except KeyError: # No, this wasn't a login attempt. Send the user to # the login "page". return loginPage(cpg.request.path) # Now look up the user id by the email and password userid = getUserId(email, password) if userid is None: # Bad login attempt return loginPage(cpg.request.path, 'Invalid email address or password.') # User is now logged in, so retain the userid and show the content cpg.request.sessionMap['userid'] = userid # If you don't want the 'login', 'loginEmail' and 'loginPassword' # values to be passed to the original method that was interrupted # by the login challenge, uncomment the following # del kwargs['login'] # del kwargs['loginEmail'] # del kwargs['loginPassword'] return fn(*args, **kwargs) return _wrapper def getUserId(email, password): '''Simple function to look up a user id from email and password. Naturally, this would be stored in a database rather than hardcoded, and the password would be stored in a hashed format rather than in cleartext. Returns the userid on success, or None on failure. ''' accounts = {('tim@lesher.ws', 'foo'): 'tim'} return accounts.get((email,password), None) @html('Sitename: Log In') def loginPage(targetPage, message=None): '''Return a login "pagelet" that replaces the regular content if the user is not logged in.''' result = [] result.append('<h1>Sitename Login</h1>') if message is not None: result.append('<p>%s</p>' % message) result.append('<form action=%s method=post>' % targetPage) result.append('<p>Email Address: <input type=text name="loginEmail"></p>') result.append('<p>Password: <input type=password name="loginPassword"></p>') result.append('<p><input type="submit" name="login" value="Log In"></p>') result.append('</form>') return '\n'.join(result) class Site: @cpg.expose @html("Sitename: Home Page") def index(self, *args, **kwargs): return '''<h1>SiteName</h1> <h2>Home Page</h2> <p><a href="public">Public Page</a></p> <p><a href="private">Private Page</a> <i>(registered users only)</i></p> ''' @cpg.expose @html("Sitename: Public Page") def public(self, *args, **kwargs): return '''<h1>SiteName</h1> <h2>Public Page</h2> <p><a href="/">Go back home</a></p>''' @cpg.expose @needsLogin @html("Sitename: Private Page") def private(self, *args, **kwargs): return '''<h1>SiteName</h1> <h2>Private Page</h2> <p><a href="/">Go back home</a></p>''' cpg.root = Site() cpg.server.start(configFile = 'site.conf')
This example uses two decorators:
- An html decorator that wraps the return value of its function in HTML tags, including a <TITLE> tag in the HTML <HEAD>.
- A needsLogin decorator that checks whether a user is logged in before providing "private" content.
I originally implemented this using cherrypy.lib.aspect, but I ran into some limitations of that class:
- You get precisely one _before and _after method for each Aspect-derived class, so you can't easily mix-and-match multiple aspects.
- If your _before handler returns STOP, your _after handler never gets called. This means that if you're trying to implement header and footer aspects, your header aspect has to do the same work as the footer aspect if it halts processing.
- The handlers don't get access to either the self object or the arguments of the method to be called.
I started implementing a version of Aspect that corrected these problems, but I soon realized I was re-implementing decorators, so I switched to a decorator-based implementation.
Attachments
- check.py (2.9 kB) -
I have modified the file to use without decorators (Python 2.3)
, added by py@topmusic.ch on 03/17/07 20:09:49.

