Web Interfaces |
Assuming that you've read and digested the previous search example, you're probably wanting to put up some sort of fancy web interface to all your data. While we can't help out with the XML to HTML stylesheet or the actual web design for your site, below is a simplified and annotated copy of the Cards, Not Words main handling functions.
01 from mod_python import apache, Cookie 02 from mod_python.util import FieldStorage 03 import sys, traceback, os, cgitb, time, urllib, crypt 04 from server import SimpleServer 05 from PyZ3950 import CQLParser 06 from baseObject import Session 07 os.chdir("/home/cheshire/cheshire3/code") 08 session = Session() 09 serv = SimpleServer(session, '/home/cheshire/cheshire3/configs/serverConfig.xml') 10 l5r = serv.get_object(session, 'db_l5r') 11 titleIdx = l5r.get_object(session, 'l5r-idx-1') 12 recStore = l5r.get_object(session, 'l5rRecordStore') 13 cardTxr = l5r.get_object(session, 'l5rHtmlTxr') 14 singleTxr = l5r.get_object(session, 'l5rSingleTxr') 15 cartTxr = l5r.get_object(session, 'l5rCartTxr') 16 rsetStore = l5r.get_object(session, 'defaultResultSetStore') 17 authStore = serv.get_object(session, 'defaultAuthStore') |
Most importantly, we need to import the linking code from mod_python (line 1). From then on, this is similar to the sort of code that you'll see in any handler, we import the server implementation, create the server and find various useful objects within the framework.
18 class CNWHandler: 19 templatePath = "/home/cheshire/cheshire3/cnw/html/template.ssi" 20 def generate_query(self, form): 21 if (form.has_key('query') and form['query'].value): 22 return urllib.unquote(form['query'].value) 23 if (form.has_key('bool')): 24 bool = form['bool'].value 25 else: 26 bool = 'and' 27 n = 1 28 cql = [] 29 while (form.has_key('idx%d' % n)): 30 try: 31 term = form['term%d' % n].value 32 except: 33 n += 1 34 continue 35 if (term): 36 idx = form['idx%d' % n].value 37 rel = form['rel%d' % n].value 39 if (cql): 40 cql.append(bool) 41 cql.append(' %s %s "%s" ' % (idx, rel, term)) 42 n += 1 43 return ''.join(cql) |
In line 18 we start a class definition. This is going to define the sort of object used to handle the generic web interface. It has one member property, the path to a template file which we'll use at the end to provide a consistent look and feel across the entire site.
As the query will most likely come in from an HTML form, we need to create the equivalent CQL search and this is what generate_query() does above. First it checks to see if there's a 'query' parameter and it's been filled out (21). If so, then unhexify it (turn all the %20s into spaces, for example) and send it straight back (22) -- the user has been kind enough to give us CQL directly. Otherwise we need to process the rest of the form.
If the form has a 'bool' field, then we'll use it (24) between each of the clauses in the CQL, otherwise we'll just default to 'and' (26). Then we step through the form looking for fields that start with 'idx' and end in a number (29). These are the names of the indexes to use. If the input element for that index has been filled out (31) we'll use it for the term, otherwise we just step back to the beginning and look for the next index (34).
If the term has been given we construct the index relation term searchClause for CQL (41) from the values given in the form (36-37) and if this is the second or subsequent clause (39) we'll also add in the boolean (40). At the end we join it all together and return it.
44 def generateCart(self, rset): 45 global titleIdx, recStore, cartTxr 46 rset.order(session, titleIdx) 47 cards = [] 48 for item in rset: 49 rec = recStore.fetch_record(session, item.docid) 50 htmldoc = cartTxr.process_record(session, rec) 51 html = htmldoc.get_raw() 52 cards.append(html) 53 cards.append('<tr><td align="right"><a href="viewCart.html">Show Cart --></a></td></tr>') 54 cards = ''.join(cards) 55 text = '<tr><td class="headCell" colspan="2">Cart:</td></tr><tr><td>%s</td></tr>' % cards 56 return text 57 58 def display_searchList(self, cql, start=0): 59 global l5r, titleIdx, recStore, singleTxr 60 try: 61 tree = CQLParser.parse(cql) 62 except: 63 return ("Search Error", "<p>Could not parse your query. Please try again.") 64 try: 65 rs = l5r.search(session, tree) 66 except Exception, err: 67 return ("Search Error", "<p>Could not process your query. Please try again.<br>Error: %s" % err) 68 rs.order(session, titleIdx) 69 d = ['<table width="95%" align="center" class="cardListTable" cellspacing=0 cellpadding=1>' <tr><th>Card Name</th><th width="8%">Stock</th><th width="8%">Price</th><th width="8%">Add</th></tr>'] 70 end = start + min(len(rs) - start, 25) 71 while (x < len(rs) and x < end): 72 rec = recStore.fetch_record(session, rs[x].docid) 73 htmldoc = singleTxr.process_record(session, rec) 74 html = htmldoc.get_raw() 75 d.append(html) 76 d.append("</table>") 77 txt = ''.join(d) 78 t = "Cards %d-%d/%d" % (start+1, min(end, len(rs)), len(rs)) 79 return (t, txt) 80 81 def display_card(self, rec, path): 82 global cardTxr 83 htmldoc = cardTxr.process_record(session, rec) 84 html = htmldoc.get_raw() 85 (title,html) = html.split('||') 86 href = path + "?c=%d&addToCart=%d" % (rec.id, rec.id) 87 return (title, "<table>%s</table>" % html) |
This section has three functions, generateCart(), display_searchList() and display_card(), but they all serve the same purpose -- to display the records in an appropriate format. The generateCart() function creates a simple display of name, price and number of entries to display for a user's shopping cart, the display_searchList() function does the same but with more information for a list of matched records, and display_card() shows the entire text of the record.
In generateCart, first we specifically allow global references (45) to the various objects that were identified in the first invocation of the script. The function is given a ResultSet object, so we sort that by the title index next to produce a more usefully ordered list (46). In the following block, we step through each entry in the result set, fetch the record (49), turn it into the appropriate format for display (50) and then add that to our list of entries. Then we put a bit of HTML wrapping (53-55) around the text we've generated and return it.
Display_searchList starts off much the same, except that it's given a CQL query to execute. First we try to parse the query into tree form (61), and if we can't we return a brief error message (63). Then we try to run the search (65) and again an error message if that fails (67). Now we have the result set, and the same thing happens. We order it by the title of the card (68), create some wrapping (69), step through the result set and get an HTML display for it (71-75), put it all together and return it (79).
The last function here, display_card, is even simpler. It's given the record to display, it processes it into HTML (83) adds some wrapping and returns the data.
88 def send_html(self, text, req, code=200): 89 req.content_type = 'text/html' 90 req.content_length = len(text) 91 req.send_http_header() 92 req.write(text) 93 94 def handle(self, req): 95 global rsetStore, recStore 96 path = req.uri[1:] 97 form = FieldStorage(req) 98 f = file(self.templatePath) 99 tmpl = f.read() 100 f.close() 101 tmpl = tmpl.replace('\n', '') 102 103 cks = Cookie.get_cookies(req) 104 if cks.has_key('cnwCart'): 105 cart = cks['cnwCart'] 106 rsid = cart.value 107 try: 108 cartRSet = rsetStore.fetch_resultSet(session, rsid) 109 except: 110 cartRSet = None 111 if (cartRSet == None): 112 cartRSet = resultSet.SimpleResultSet(session) 113 rsid = rsetStore.create_resultSet(session, cartRSet) 114 cart = Cookie.Cookie('cnwCart', rsid) 115 else: 116 cartRSet = resultSet.SimpleResultSet(session, []) 117 rsid = rsetStore.create_resultSet(session, cartRSet) 118 rsetStore.commit_storing(session) 119 cart = Cookie.Cookie('cnwCart', rsid) 120 Cookie.add_cookie(req, cart) 121 122 if (path == "card.html"): 123 if (form.has_key('c')): 124 id = form['c'].value 125 rs = l5r.get_object(session, "l5rRecordStore") 126 rec = rs.fetch_record(session, id) 127 (t, d) = self.display_card(rec, req.uri) 128 else: 129 (t,d) = ("Error", "<p>You must give a card id to display</p>") 130 elif (path =="list.html"): 131 if (form.has_key('start')): 132 start = int(form['start'].value) 133 else: 134 start = 0 135 cql = self.generate_query(form) 136 (t, d) = self.display_searchList(cql, start=start, url=path) 137 else: 138 if (os.path.exists(path)): 139 f = file(path) 140 d = f.read() 141 f.close() 142 stuff = d.split("\n", 1) 143 if (len(stuff) == 1): 144 t = "Cards, Not Words" 145 else: 146 (t, d) = stuff 147 else: 148 t = "Page Not Found" 149 d = "<p>Could not find your requested page: '%s'</p><p>Please try again.</p>" % path 150 151 cart = self.generateCart(cartRSet) 152 tmpl = tmpl.replace("%CONTENT%", d) 153 tmpl = tmpl.replace("%CONTENTTITLE%", t) 154 tmpl = tmpl.replace("%CARTINCLUDE%", cart) 155 self.send_html(tmpl, req) |
While most of the work as far as display is done in the previous section, we still need to handle the HTTP side of things a bit more to get the right bits and pieces to those routines. Here we have a quick function to return the HTML back to the browser, and the function called to handle the Apache request.
Send_html() simply sets up the request object and then sends the HTML back to the browser. It sets the content_type (89), the length (90), sends the HTTP headers (91) and the HTML (92). Easy.
First we cut off the leading '/' from the URI so we can from now on treat it as a path on disk (96) and create an object to handle any form data in the request (97). So that our HTML template is always current, we read it in every time (98-101).
Once that initial processing is out of the way, we do some Cookie tricks. We use a cookie called 'cnwCart' to store a ResultSet identifier to use as a shopping cart. If it exists already (104), we try to fetch the result set from storage (108). If that fails (eg the result set has expired), then we create a new one and set a Cookie to record the new identifier (112-114). If there's no shopping cart cookie, then we create one (116-119). Line 120 then adds the cookie to be sent back to the browser.
Lines 122-129 handle the dynamic page used to display a single card (record), 130-136 display a list of matching titles, and 137 through to 149 handles everything else (static pages and 404 errors). First the single card. We check to see if the request has the card id (123), otherwise we give an error message (129). If the request is okay, then we fetch the record from the recordStore (126) and call the display_card() function discussed previously.
For card lists, we check to see the start position in the result set, defaulting to 0 (131-134), generate the query (135) and use the display code above to render the list (136).
Otherwise it's just a static page, so we check to see if it exists (138) otherwise return a 404 style error (147-149). If it does exist, we read it in, treating the first line as the title in the template.
Finally we generate the shopping cart HTML (151), do some replaces in the template (152-154) and send the HTML back to the browser (155). Phew! :)
156 def handler(req): 157 os.chdir("/home/cheshire/cheshire3/www/cnw/html") 158 cnwhandler = CNWHandler() 159 try: 160 cnwhandler.handle(req) 161 except: 162 req.content_type = "text/html" 163 cgitb.Hook(file = req).handle() 164 return apache.OK 165 166 def authenhandler(req): 167 pw = req.get_basic_auth_pw() 168 user = req.user 169 u = authStore.fetch_object(session, user) 170 if (u and u.password == crypt.crypt(pw, pw[:2])): 171 return apache.OK 172 else: 173 return apache.HTTP_UNAUTHORIZED |
We also need some standard functions for the mod_python apache handler to call. The first, handler(), processes all incoming requests. It moves to the appropriate base directory where the HTML is kept (157) and creates an object to handle the request (158). It then tells the object to process it (160), and if it errors, it uses the stock Python cgi traceback display code (163).
To allow for an administration interface, we have an authentication handler. All this needs to do is return whether or not the user is authorised (171) or not (173). To do this, it checks the password given to apache (167) against the password on the user object from a Cheshire3 userStore (169).
And that's it :) A web site handled by Cheshire3.
You can find the full example code, with all the extra bits and pieces omitted for sanity above, in the Cheshire3 distribution.