XLP Literate Program Template


Version 0.8.3


Table of Contents

1 Introduction
2 The Wine Module
2.1 Imports
2.2 define the wineHeaders routine
2.3 Define the Wine Class
2.3.1 Define the wineClass Initialization
2.3.2 Define the wineClass String Representation
2.3.3 Define the wineClass toHTML Function
2.3.4 Define the wineClass Update Procedure
2.4 Define the Wine Catalog Class
2.4.1 Define the loadCatalog Routine
2.4.2 Define the saveCatalog Routine
2.4.3 Render as HTML
2.4.4 define the displayRack2 routine
2.4.5 define the displayDrunks routine
2.4.6 Define the nextWine Generator
2.4.7 define the loadWineRack routine
2.5 Define the Wine Rack Class
2.6 Define the Wine Box Class
3 The wine Program
3.1 define the doWine routine
3.2 define the editWine routine
3.3 define the displayRack routine
3.4 The wine.py Main Program
3.4.1 wine.py Determine the Server and Host Names
3.4.2 Determine the Server and Host Names
3.4.3 Integrity and Debug Tests
3.4.4 create and load the wine catalog
3.4.5 Collect Invocation Parameters
3.4.6 Process Request String
3.4.7 Determine Action to be Performed
4 The wineEdit Program
6 The Makefile
7 Document History
8 Indices
8.1 Files
8.2 Chunks
8.3 Identifiers

1. Introduction

This program suite provides an interactive web-based interface to the wine database.

There are several programs in this suite:

  1. The wine module file wineModule.py. Defines all parameters associated with a given wine in the collection (the wineClass), together with a catalog of all known wines (the wineCatalog class).
  2. The wine web program wine.py. This program is called from web pages, and renders various views of the wine database.
  3. The wine edit program wineEdit.py. This program is called from the web page generated by wine.py when it is desired to edit a wine record. It returns to wine.py when finished.

2. The Wine Module

"wineModule.py" 2.1 =
## P R O G R A M N A M E ## ## ********************************************************** ## * DO NOT EDIT THIS FILE! * ## * Use $HOME/Computers/Sources/Wine/wine.xlp instead * ## ********************************************************** ## <imports 2.2> <define the wine class 2.4> <define the wine catalog class 2.15> <define the wine rack class 2.29> <define the wine box class 2.30> # Header Generation <wine.py define the wineHeaders routine 2.3> server='localhost' debug=0 def main(): wr=wineRackClass(server) #print wr wc=wineCatalog(server) wc.loadCatalog() wc.loadWineRack(server) #print wc.wineRack print wc.displayRack2('true') sys.exit(0) wc=wineCatalog(server) wc.loadCatalog() wc.saveCatalog() #print wc.makeHTML() if __name__ == '__main__': main() ## ## The End ##

The wineModule provides importable definitions of the wineClass and the wineCatalog. When invoked as a standalone program, it reads the catalog, prints it out, and then saves it (unchanged). This is for testing purposes.

2.1 Imports

<imports 2.2> =
from lxml import etree as ET import re import sys import cgitb
Chunk referenced in 2.1

The main thing to note here is that this program saves its database in an XML format, and we use the lxml element tree library to handle translation of the database to and from the internal representation (described later).

2.2 define the wineHeaders routine

<wine.py define the wineHeaders routine 2.3> =
def wineHeaders(server): cgiScript="http://{}/~ajh/cgi-bin/wine.py?".format(server) str='' str+="<a href=\"{}".format(cgiScript)+\ "sort=vintage\">Wines by Vintage</a> : " str+="<a href=\"{}".format(cgiScript)+\ "sort=label\">Wines by Label</a> : " str+="<a href=\"{}".format(cgiScript)+\ "action=boxes\">Wines by Boxes</a> : " str+="<a href=\"{}rack=true\">Wine Rack</a> ".format(cgiScript) str+="<a href=\"{}rack=other\">(other,</a> ".format(cgiScript) str+="<a href=\"{}rack=red\">red,</a> ".format(cgiScript) str+="<a href=\"{}rack=white\">white) : </a> ".format(cgiScript) str+="<a href=\"{}action=new\">Add New Wine : </a>\n".format(cgiScript) str+="<a href=\"{}action=drunk\">Show Drunk Wines</a>\n".format(cgiScript) return str
Chunk referenced in 2.1

2.3 Define the Wine Class

<define the wine class 2.4> =
class wineClass(): <define the wineClass initialization 2.5> <define the wineClass string representation 2.6> <define the wineClass toHTML function 2.7> <define the wineClass update procedure 2.12>
Chunk referenced in 2.1

The wineClass class encapsulates all working data associated with identifying a given wine and its properties.

2.3.1 Define the wineClass Initialization

<define the wineClass initialization 2.5> =
def __init__(self,server): self.vintage=0 self.quantity=0 self.stash=[] self.drunk=[] self.type='red' self.variety='shiraz' self.company='' self.label='' self.cost=0.0 self.request='' self.server=server
Chunk referenced in 2.4

Most of these fields are self explanatory, except perhaps self.stash. It will need to hold multiple instances of locations of the wine, together with the number of bottles in each location. Suggest an array of tuples '(location,number)'? An integrity constraint is that self.quantity should at all times equal the sum of each number in the stash array. Suggest also some form of location shorthand for bottles across a set of contiguous locations 'a01-06'

The field drunk is intended to capture information about a bottle of this wine being drunk. This will include the date of drinking/opening, and any comments to be made about the wine. There may be zero or more instances of such detail, and hence an array of tuples sounds like the best way to structure this.

The internal data structure of the wineClass is

A (perhaps shortened form of the) wine label. It is intended to be a unique key to the wine itself. It is a text string of arbitrary length, although some usages may restrict the amount displayed.
The vintage year of the wine. Non-vintage wines have a vintage year of 0. This must be an integer (although this property is never tested or utilized). It should be consistent with any vintage indication in the label, although again, this is never checked for consistency.
The total number of bottles held in stock. This must be consistent with the various stash amounts (see below). It must be an integer.
The type of the wine, whether sparking, red, white, fortified, etc.. A text string.
The grape variety used in making the wine. A text string.
The wine making company. A text string. This value is not currently used, and will be added to the database at a future date.
The cost of the wine. A real number. This is assumed to be fixed across all instances of purchase, although it is acknowledged that possible variations may occur. This anomaly will be addressed in future versions.
A list of the various locations where the wine is held. Each entry in the list is a tuple (quantity,location) where quantity (an integer) is the number of bottles held in the particular location (a text string). A shorthand exists where several bottles may be held in adjacent locations, which takes the form loc{start}-{finish}, where start and finish are integers identifying a contiguous set of locations, and loc is the base location of this particular stash.
A list of dates when the wine was drunk, along with a comment (possibly empty) for each occasion. Each entry in the list is a tuple (d,c), where d is the date and c is the comment.

2.3.2 Define the wineClass String Representation

<define the wineClass string representation 2.6> =
def __str__(self): res="{}:\n".format(self.label) res+=" vintage:{}\n".format(self.vintage) res+=" quantity:{}\n".format(self.quantity) res+=" stash:{}\n".format(self.stash) res+=" drunk:{}\n".format(self.drunk) res+=" type:{}, variety:{}\n".format(self.type,self.variety) res+=" company:{}\n".format(self.company) res+=" cost:{}\n".format(self.cost) return res
Chunk referenced in 2.4

This is a simple transliteration of the wine values into a text string. Intended for debugging and not much else.

2.3.3 Define the wineClass toHTML Function

<define the wineClass toHTML function 2.7> =
def toHTML(self): server=self.server #print "1:server={}".format(server) str="<html>\n" str+=" <head>\n" str+=" <title>{0}</title>\n".format(self.label) str+=" </head>\n" str+=" <body>\n" str+=" <h1>" str+=wineHeaders(server) str+="<a href=\"http://{}/{}\">Previous View</a> ".format(server,self.request) {Note 2.7.1} str+="</h1>\n" str+=" <form method='post' action='http://{0}/~ajh/cgi-bin/wineEdit.py?request={1}'>\n".format(server,self.request) {Note 2.7.2} str+=" <button name='save' value='{}'>SAVE</button> (click any link above to quit editing)\n".format(self.label) str+=" <table>\n" <generate toHTML form for line 1 (LABEL) 2.8> <generate toHTML form for line 2 (VINTAGE,QUANTITY,STASHES) 2.9> <generate toHTML form for line 3 (COST,TYPE,VARIETY) 2.10> <generate toHTML form for lines 4 et seq (DRINKING) 2.11> str+=" </table>\n" str+=' </form>\n' str+=' </body>\n' str+="</html>\n" return str
Chunk referenced in 2.4
{Note 2.7.1}
See the second paragraph below
{Note 2.7.2}
See the second paragraph below

The toHTML function is responsible for generating an HTML page that allows editing of a wine. It does this by creating a form containing several INPUT style HTML elements, one for each data component of a wine. These are structured three to a line, except for the longer components, especially the drinking data, where one line per drink record is used. The last line of this collection is always blank, to allow new drink records to be created.

Note that when the SAVE button is pressed, a call on the wineEdit script is made, and the current request that brought us to this point is also passed to the script as the request parameter. This is so that the editing script has a pathway to return to the original invoking script. generate toHTML line 1

<generate toHTML form for line 1 (LABEL) 2.8> =
str+=" <tr>\n" str+=" <td colspan='3'>\n" str+=" <fieldset>" str+=" <legend><b>LABEL:</b></legend>\n" str+=" <input type='text' name='label' size='100%' value='{0}'>\n".format(self.label) str+=" </fieldset>\n" str+=" </td>\n" str+=" </tr>\n"
Chunk referenced in 2.7 generate toHTML line 2

<generate toHTML form for line 2 (VINTAGE,QUANTITY,STASHES) 2.9> =
str+=" <tr>\n" str+=" <td>\n" str+=" <fieldset>" str+=" <legend><b>VINTAGE:</b></legend>\n" str+=" <input type='text' name='vintage' size='10' value='{0}'>\n".format(self.vintage) str+=" </fieldset>\n" str+=" </td>\n" str+=" <td>\n" str+=" <fieldset>" str+=" <legend><b>QUANTITY:</b></legend>\n" str+=" <input type='text' name='quantity' value='{0}'><br/>\n".format(self.quantity) str+=" </fieldset>\n" str+=" </td>\n" str+=" <td>\n" str+=" <fieldset>" stash=sep='' for (q,l) in self.stash: stash+="{}{}@{}".format(sep,q,l) sep=',' str+=" <legend><b>STASHES:</b></legend>\n" str+=" <input type='text' name='stash' value='{0}' size='40'><br/>\n".format(stash) str+=" </fieldset>\n" str+=" </td>\n" str+=" </tr>\n"
Chunk referenced in 2.7 generate toHTML line 3

<generate toHTML form for line 3 (COST,TYPE,VARIETY) 2.10> =
str+=" <tr>\n" str+=" <td>\n" str+=" <fieldset>" str+=" <legend><b>COST:</b></legend>\n" str+=" <input type='text' name='cost' value='{0}'><br/>\n".format(self.cost) str+=" </fieldset>\n" str+=" </td>\n" str+=" <td>\n" str+=" <fieldset>" str+=" <legend><b>TYPE:</b></legend>\n" str+=" <input type='text' name='type' size='10' value='{0}'>\n".format(self.type) str+=" </fieldset>\n" str+=" </td>\n" str+=" <td>\n" str+=" <fieldset>" str+=" <legend><b>VARIETY:</b></legend>\n" str+=" <input type='text' name='variety' value='{0}' size='40'><br/>\n".format(self.variety) str+=" </fieldset>\n" str+=" </td>\n" str+=" </tr>\n"
Chunk referenced in 2.7 generate toHTML line 4

<generate toHTML form for lines 4 et seq (DRINKING) 2.11> =
str+=" <tr>\n" str+=" <td colspan='3'>\n" str+=" <fieldset>" str+=" <legend><b>DRINKING:</b></legend>\n" i=0 for (d,c) in self.drunk: drink="{}: {}".format(d,c) str+=" <input type='text' name='drunk{}' value='{}' size='100%'><br/>\n".\ format(i,drink) i+=1 str+=" <input type='text' name='drunk{}' value='' size='100%'><br/>\n".format(i) str+=" </fieldset>\n" str+=" </td>\n" str+=" </tr>\n"
Chunk referenced in 2.7

2.3.4 Define the wineClass Update Procedure

<define the wineClass update procedure 2.12> =
def update(self,form): keys=form.keys() keys.sort() for k in ['vintage','quantity','stash','cost','type','variety','label']: <wineClass Update each field 2.13> <wineClass Update the drunk fields 2.14> return
Chunk referenced in 2.4 wineClass Update each field

<wineClass Update each field 2.13> =
if debug>0: print "<p>Processing key {0}:".format(k) if not form.has_key(k): continue value=form[k].value if k=='stash': # convert string rep to array rep stash=[] stashes=value.split(',') for st in stashes: (q,l)=st.split('@') stash.append((q,l)) value=stash if k in keys: self.__dict__[k]=value if debug>0: print "updated to {0}".format(value) print self,self.__dict__ else: self.__dict__[k]='' if debug>0: print "cleared"
Chunk referenced in 2.12

This fragment is invoked for each wine field in the upper level for loop. stash is a special case, because of the multiple values within the field, but otherwise the new field values are copied into the eponymously label field via the class dictionary.

keys is a list variable containing all keys that are to be updated. wineClass Update the drunk fields

<wineClass Update the drunk fields 2.14> =
i=0 drunk=[] key='drunk{}'.format(i) if debug: print "Form has keys={}".format(form.keys()) print "Testing key {}".format(key) while form.has_key(key): value=form[key].value if debug: print "Got drink value {}".format(value) res=re.match('^(\d+):(.*)$',value) if res: date=res.group(1) comment=res.group(2) drunk.append((date,comment)) i+=1 key='drunk{}'.format(i) if debug: print "Testing key {}".format(key) self.drunk=drunk
Chunk referenced in 2.12

The handling of drunk values is complicated by the fact that there are an arbitrary number of them, each with their own name field in the web form. We have to process each such name separately.

2.4 Define the Wine Catalog Class

The following definition is pinched straight from the first prototype. It is being modified to suit. Remove this comment once that is complete.

<define the wine catalog class 2.15> =
class wineCatalog(): def __init__(self,server): self.wines=[] self.dict={} # indexed by 'label' (wines) self.boxes={} # indexed by 'label' (boxes) self.i=0 self.n=0 #print "catalog server set as {}".format(server) self.server=server pass def __iter__(self): return self def reset(self): self.i=0 self.n=len(self.wines) def next(self): if self.i<self.n: w=self.wines[self.i] self.i+=1 return w else: self.reset() raise StopIteration() <define the loadCatalog routine 2.16> <define the saveCatalog routine 2.20> <define the makeHTML routine 2.21,2.22,2.23> <define the displayRack2 routine 2.25> <define the displayDrunks routine 2.26> <define the nextWine generator > <define the loadWineRack routine 2.28>
Chunk referenced in 2.1

The wineCatalog class holds multiple instances of wine objects. Besides the initialization routine, there are methods to load and save the catalog of wines from the external file representation, to render the catalog into an HTML version, and several methods to make a catalog object an iterable over all wines stored therein.

2.4.1 Define the loadCatalog Routine

<define the loadCatalog routine 2.16> =
def loadCatalog(self,filename='/home/ajh/www/personal/wine/WineCatalog.xml'): # get the dom try: winetree=ET.parse(filename) root=winetree.getroot() except: print "could not parse the wine xml file" sys.exit(1) for wine in root.iter("wine"): <get details for this wine entry 2.17> <add wine labelled label to catalog 2.18> for wine in root.iter("box"): <get box details from database 2.19>
Chunk referenced in 2.15

Open the XML database file and parse the contents as a DOM tree (held in variable winetree). Extract the wine entries from the list defined in the root of this tree (labelled winecatalog2 in the XML file). If this succeeds, we iterate through the list root, and process each wine entry found there. get details for this wine entry

<get details for this wine entry 2.17> =
date=consumed=cost='' drunk=[] ; stash=[] for field in wine: if field.tag == 'vintage': vintage=field.text elif field.tag == 'quantity': quantity=field.text if not quantity: quantity=0 elif field.tag == 'stash': location=field.text # parse the location into an array of (q,l) if location: qlocs=location.split(',') for ql in qlocs: res=re.match('(\d+)@(([a-zA-Z0-9 ]+)(-[0-9]+)?)',ql) if res: q=res.group(1) l=res.group(2) stash.append((q,l)) elif field.tag == 'type': type=field.text elif field.tag == 'variety': variety=field.text elif field.tag == 'label': label=field.text elif field.tag == 'drunk': #print "\n{}".format(label) #print "New drink entry, current = {}".format(drunk) if field.attrib.has_key('date'): date=field.attrib['date'] if not drunk: # make new array drunk=[(date,field.text)] else: # previous entry, append to it drunk.append((date,field.text)) #print "Updated drink = {}".format(drunk) elif field.tag == 'cost': cost=field.text else: pass
Chunk referenced in 2.16 add wine labelled label to catalog

<add wine labelled label to catalog 2.18> =
if self.dict.has_key(label): # already know about this wine, update its details thiswine=self.dict[label] # vintage should be the same # quantity and stash must be updated thiswine.stash.extend(stash) thiswine.quantity+=int(quantity) # type should be the same # variety should be the same # label is by definition the same # consumed must be updated if date or consumed: #print "New drink (previous) entry, current = {}".format(drunk) drunk.append((date,consumed)) #print "Updated drink = {}".for mat(drunk) # cost might be different, need to figure out what to do about this if cost: if not thiswine.cost: thiswine.cost=cost else: # new wine, create an entry for it thiswine=wineClass(self.server) thiswine.vintage=vintage #print quantity thiswine.quantity=int(quantity) thiswine.stash=stash thiswine.type=type thiswine.variety=variety thiswine.label=label thiswine.drunk=drunk thiswine.cost=cost self.dict[label]=thiswine self.wines.append(thiswine)
Chunk referenced in 2.16

We now have all the information pertaining to the next wine entry. We need to check if this wine is held already, and if so, add the wines in the new entry to the existing holdings. This will then appear as a single entry when the catalog is next saved. If the wine is not known, add it as a new entry in the catalog. Get Box Details from Database

<get box details from database 2.19> =
label=location='' for field in wine: if field.tag=='label': label=field.text elif field.tag=='location': location=field.text else: print "Invalid field in box element: {}".format(field.tag) if label and location: self.boxes[label]=location
Chunk referenced in 2.16

2.4.2 Define the saveCatalog Routine

<define the saveCatalog routine 2.20> =
def saveCatalog(self,filename='/home/ajh/www/personal/wine/WineCatalog.xml'): keys=self.dict.keys() keys.sort() newcat=ET.Element('winecatalog2') for k in keys: wine=self.dict[k] newwine=ET.Element('wine') newvint=ET.SubElement(newwine,'vintage') newvint.text=wine.vintage newquant=ET.SubElement(newwine,'quantity') newquant.text="{}".format(wine.quantity) newstash=ET.SubElement(newwine,'stash') #print wine.stash stashstr=sep='' checkquant=0 for (q,l) in wine.stash: checkquant+=int(q) stashstr+="{}{}@{}".format(sep,q,l) sep=',' if checkquant!=wine.quantity: print "quantity consistency check: {} v {} - updated".\ format(checkquant,wine.quantity) newquant.text="{}".format(checkquant) #print stashstr newstash.text=stashstr newtype=ET.SubElement(newwine,'type') newtype.text=wine.type newvariety=ET.SubElement(newwine,'variety') newvariety.text=wine.variety newlabel=ET.SubElement(newwine,'label') newlabel.text=wine.label for (d,c) in wine.drunk: newdrunk=ET.SubElement(newwine,'drunk') newdrunk.text=c newdrunk.attrib['date']=d newcost=ET.SubElement(newwine,'cost') newcost.text=wine.cost newcat.append(newwine) keys=self.boxes.keys() keys.sort() for box in keys: loc=self.boxes[box] newbox=ET.Element('box') newlabel=ET.SubElement(newbox,'label') newlabel.text=box newloc=ET.SubElement(newbox,'location') newloc.text=loc newcat.append(newbox) # all done, now output the XML serialization xmlstring=ET.tostring(newcat,pretty_print=True) fh=open(filename,'w') fh.write(xmlstring) fh.close()
Chunk referenced in 2.15

2.4.3 Render as HTML

<define the makeHTML routine 2.21> =
def makeHTML(self,filter=None,sortkey='vintage',requestString='',showPrevious=False): if debug: print "<p>Filter by {},Sort by {}".format(filter,sortkey) error=0 # sanitize filter ffield=vfield=None if filter: res=re.match('^([a-z]+)=([a-zA-Z0-9]+)$',filter) if res: ffield=res.group(1) vfield=res.group(2) if debug: print "got filter, {}={}".format(ffield,vfield) html=''
Chunk referenced in 2.15
Chunk defined in 2.21,2.22,2.23

We start making an HTML rendering of all the wines by first extracting any filter specification.

<define the makeHTML routine 2.22> =
def cmp(a,b): if eval('a.{}'.format(sortkey)) > eval('b.{}'.format(sortkey)): return 1 if eval('a.{}'.format(sortkey)) < eval('b.{}'.format(sortkey)): return -1 return 0
Chunk referenced in 2.15
Chunk defined in 2.21,2.22,2.23

Define the comparision routine for sorting the wines. This is given by the sort parameter, which specifies which field of a wine entry is the sort key. Because this is dynamically specified, we need to use an eval to determine which field is to be used.

<define the makeHTML routine 2.23> =
wines=self.wines wines.sort(cmp) html+='<table border="1">\n' count=0 for w in wines: <makeHTML process a single wine label 2.24> html+='</table>\n' if showPrevious: count=0 dozens=count / 12 spare=count % 12 html+="<p>wines in this selection = {} ({}/12 + {})</p>\n".\ format(count,dozens,spare) html+="<p><a href='http://{}/~ajh/computing/wine.html' target='blank'>".format(self.server) html+="Visit source code</a></p>\n" if error: print "<p>Bad filter: '{}'</p>".format(filter) return html
Chunk referenced in 2.15
Chunk defined in 2.21,2.22,2.23

Loop over all wines in the catalog, generating an HTML table line for each one that is specified by the filter.

<makeHTML process a single wine label 2.24> =
thiscount=0 #print "Processing wine {}, filter={}".format(w,filter) w.request=requestString if filter: value='' try: #print eval('w.{}'.format(ffield)) value="{}".format(eval('w.{}'.format(ffield))) if vfield not in value: continue except: if not value: continue # type any empty/None values error=1 #print "This was bad: {}/{}".format(ffield,value) #print w thiswine=' <tr>\n' thiswine+=' <td>{}</td>\n'.format(w.vintage) thiswine+=' <td>{}</td>\n'.format(w.quantity) stash=w.stash sep='' thiswine+=' <td>' for (q,l) in stash: if int(q)==0: continue thiswine+='{}{}@{}'.format(sep,q,l) count+=int(q) thiscount+=int(q) sep=',' thiswine+='</td>\n' thiswine+=' <td>{}</td>\n'.format(w.type) thiswine+=' <td>{}</td>\n'.format(w.variety) thiswine+=' <td><a href="http://{}/~ajh/cgi-bin/wine.py?edit={}&request={}">{}</a></td>\n'.format(self.server,w.label,w.request,w.label) thiswine+=' <td>{}</td>\n'.format(w.cost) drunk='-' if w.drunk: drunk='<a href="http://{}/~ajh/cgi-bin/wine.py?edit={}&request={}">notes</a></td>\n'.format(self.server,w.label,w.request) thiswine+=' <td>{}</td>\n'.format(drunk) thiswine+=' </tr>\n' if (thiscount and not showPrevious) or (showPrevious and not thiscount): html+=thiswine
Chunk referenced in 2.23

Generate a single HTML table line for this wine. Note that only wines that pass the filter are included, and wines that have no holdings (quantity=0) are not included (this will be subject to further refinement).

The line is built up in the text string thiswine, which is added to the generated HTML string only if the count for this wine thiscount is non-zero. A wine that does not pass the filter is excluded early in the cycle by the continue statements, and no text string for this wine is added.

2.4.4 define the displayRack2 routine

<define the displayRack2 routine 2.25> =
def displayRack2(self,action): server=self.server winerack=self.wineRack header='<html>\n<head>\n<link rel="stylesheet" HREF="/~ajh/styles/wine.css" type="text/css" />\n</head>\n<body>\n<table border="1">\n' rackstr=header cgiscript="http://localhost/~ajh/cgi-bin/wine.py" rackstr+=wineHeaders(server) # now determine what section of rack if action=='true': rackStart=1; rackEnd=winerack.RackWidth elif action=='white': rackStart=1; rackEnd=8 elif action=='red': rackStart=9; rackEnd=20 elif action=='other': rackStart=21; rackEnd=winerack.RackWidth count=0; empties=0 labelrow=' <tr>\n <th width="2%">row</th>\n' for i in range(rackEnd,rackStart-1,-1): labelrow+=" <th width='4%'>{}</th>\n".format(i) labelrow+=" <th width='2%'>row</th>\n" rackstr+=labelrow for row in winerack.rows: entry="<tr>\n <td align='center'>{}</td>".format(row) for col in range(rackEnd,rackStart-1,-1): wine=winerack.getWine(row,col) label='Empty?'; bgc='grey'; winestr='' if wine: label=wine.label if label=='Blank': bgc="black" winestr='Blank' elif label=='Empty?': empties+=1 bgc="grey" winestr='Empty' else: count+=1 label=wine.label if wine.type=='red': bgc='red' elif wine.type=='white': bgc='white' elif wine.type=='rose': bgc='pink' elif wine.type=='dessert': bgc='yellow' elif wine.type=='sparkling': bgc='green' elif wine.type=='fortified': bgc='palepurple' else: bgc='olive' winestr='<a href="wine.py?edit={}">{}</a>'.format(label,label) entry+=' <td bgcolor="{}">{}</td>\n'.format(bgc,winestr) else: empties+=1 entry+=' <td bgcolor="grey"/>\n' entry+=" <td align='center'>{}</td>".format(row) entry+='</tr>\n' rackstr+=entry trailer='</table>\n' rackstr+=trailer dozens="{}+{}/12".format(count / 12, count % 12) rackstr+="<p>This section of rack contains {} bottles ({} dozens) There are {} empty slots</p>\n".format(count,dozens,empties) return rackstr
Chunk referenced in 2.15

2.4.5 define the displayDrunks routine

<define the displayDrunks routine 2.26> =
def displayDrunks(self): def mostrecent(wine): # return the most recent instance of drinking this wine if not wine.drunk: return 0 recent=0 for d,c in wine.drunk: if d.isdigit(): r = int(d) else: r = 0 if r>recent: recent=r return recent print "<table>" wines=self.wines def sortfun(a,b): return cmp(mostrecent(a),mostrecent(b)) wines.sort(sortfun) for w in wines: if w.quantity>0: continue print " <tr>" for f in ['vintage','type','variety','label']: v=w.__dict__[f] print " <td>{}</td>".format(v) wasdrunk=mostrecent(w) if not wasdrunk: wasdrunk='' print " <td>{}</td>".format(wasdrunk) drunk=w.drunk print "<td>" for d,c in drunk: print "{}: {}<br/>".format(d,c) print "</td>" print " </tr>" print "</table>\n"
Chunk referenced in 2.15

Print a list of wines for which stocks are no longer held. These are sorted in ascending order of date drunk.

The local function mostrecent is used to compute the most recent date of drinking. This is done on the assumption that the date is given in the form YYYYMMDD, which when converted to an integer and sorted, sorts into chronological order. Wines for which no date of drinking is given are given a drinking date of 0, and hence sort first.

2.4.6 Define the nextWine Generator

<define the nextwine generator 2.27> =
def nextWine(self): for wine in self.wines: yield(wine)

2.4.7 define the loadWineRack routine

<define the loadWineRack routine 2.28> =
def loadWineRack(self,server): self.wineRack=wineRackClass(server) rows=['a','b','c','d','e','f','g','h','i','j','k','l'] for wine in self.wines: #print "loading wine:{}".format(wine) for (qty,loc) in wine.stash: if int(qty)==0: continue # empty holding, ignore res=re.match('({})(\d\d)(-(\d\d))?'.format(rows),loc) if res: # bottle(s) go in wine rack row=res.group(1) #print "row={}".format(row) start=int(res.group(2)) endd=res.group(4) if endd: endd=int(endd) else: endd=start #print "start={}, end={}".format(start,endd) while start<=endd: target=self.wineRack.getWine(row,start) if target: print "<b>Two bottles in same location ({},{}): ".\ format(row,start), print "{} and {}</b>".format(target,wine.label) self.wineRack.putWine(row,start,wine) start+=1 #print "placed bottle(s) in {}".format(loc) #print self.wineRack
Chunk referenced in 2.15

2.5 Define the Wine Rack Class

The wineRack module takes responsibility for defining all the wine rack related features. A first pass "back of the envelope" analysis suggests a rectangular array of bottle slots, with the following parameters:

the width of the rack array
the height of the rack array
the rectangular array itself, with elements of type wineClass.
an object of type wineClass indicating no bottle can be stored in this location. This allows non-rectangular subsets of the WineArray.
a procedure called by the __init__ method that inserts NoSlot entries into the WineArray as required.

<define the wine rack class 2.29> =
class wineRackClass(): RackWidth=29 rows=['l','k','j','i','h','g','f','e','d','c','b','a'] RackHeight=len(rows) def __init__(self,server): self.server=server self.NoSlot=wineClass(server) self.NoSlot.label='Blank' wa={} for i in self.rows: wa[i]=[None for j in range(self.RackWidth)] self.WineArray=wa self.initWineArray() pass def initWineArray(self): # ('l'..'k',1-20) slots are non existent for i in ['l','k']: for j in range(1,21,+1): self.putWine(i,j,self.NoSlot) for i in ['l','k','j','i','h','g','f','e']: self.putWine(i,self.RackWidth,self.NoSlot) def getWine(self,row,col): wine=self.WineArray[row][self.RackWidth-col] return wine def putWine(self,row,col,wine): #print "putting wine into row='{}',col={}".format(row,col) rowentry=self.WineArray[row] if rowentry==self.NoSlot: # attempt to fetch from non-existent slot print "No slot at {},{}".format(row,col) return #print "retrieve row={}".format(rowentry) rowentry[self.RackWidth-col]=wine def __str__(self): str='' for i in self.rows: for j in range(self.RackWidth,0,-1): #print i,j wine=self.getWine(i,j) if not wine: str+='B' else: if wine and wine.label: str+=wine.label else: str+='E' str+=';' str+='\n\n' return str pass
Chunk referenced in 2.1

2.6 Define the Wine Box Class

The WineBoxClass takes responsibility for organizing where each wine box is, along with its contents. It is intended to be invoked by the cgi action of "Wine Boxes", akin to the Wine Rack operations.

A dictionary indexed by box number, with each entry a list of WineClass objects defining what wines the box contains

For the purposes of collecting all wines on offer, the stash location 'dining' is also regarded as a box, and warrants special treatment.

<define the wine box class 2.30> =
class WineBoxClass(): def __init__(self,cat): self.boxes={} self.cat=cat wines=cat.wines for w in wines: for (q,l) in w.stash: if not (l[0]=='W' or l=='dining'): continue else: # we do have a wine box entry # add this wine to this box if self.boxes.has_key(l): # already have box, extend its holdings self.boxes[l].append(w) else: # don't have this box, so add it self.boxes[l]=[w] pass pass boxes=[] keys=self.boxes.keys() keys.sort() for k in keys: boxes.append((k,self.boxes[k])) self.boxlist=boxes return def doBoxes(self): print wineHeaders(self.cat.server) print '<p>\n<table>' for (box,wines) in self.boxlist: boxloc='' if self.cat.boxes.has_key(box): boxloc='@{}'.format(self.cat.boxes[box]) print '<tr>' print ' <td>{}{}:</td><td></td>'.format(box,boxloc) print '</tr>' for wine in wines: print '<tr>' print '<td></td><td>',wine,'</td>' print '</tr>' print '</table>'
Chunk referenced in 2.1

3. The wine Program

"wine.py" 3.1 =
#!/home/ajh/binln/python2.7 # version="1.1.0" import cgi ; import cgitb ; cgitb.enable() import commands import datetime import os, os.path import re import sys from subprocess import PIPE,Popen import time import urllib2 import urlparse import xml.etree.ElementTree as ET import wineModule # globals RackWidth=29 # time stamps now=datetime.datetime.now() tsstring=now.strftime("%Y%m%d:%H%M") todayStr=now.strftime("%d %b %Y") # determine which host/server environment host=commands.getoutput('hostname') host=re.split('\.',host)[0] # break off leading part before the '.' char # globals debugFlag=False returnXML=False convertXML=False alreadyHTML=False cachedHTML=False xslfile="" # utility procedures # (None)
Chunk defined in 3.1,3.2
"wine.py" 3.2 =
######################################################################## # # # Wine Display Processing # # # ######################################################################## # Action Wines ----------------------------------------------------------- <wine.py define the doWine routine 3.3> # Edit Wines ----------------------------------------------------------- <wine.py define the editWine routine 3.4> # Display Wine by Rack ------------------------------------------------- <wine.py define the displayRack routine 3.5> # Main Program -------------------------------------------------------- <wine.py main program 3.6>
Chunk defined in 3.1,3.2

Start the HTML output conventions. Note that we use a specific css style sheet.

3.1 define the doWine routine

<wine.py define the doWine routine 3.3> =
def doWine(cat,action): if action=='new': new=wineModule.wineClass(cat.server) new.label='new' new.vintage='0' new.cost='' cat.wines.append(new) cat.dict['new']=new cat.saveCatalog() editWine(cat,'new') elif action=='boxes': boxlist=wineModule.WineBoxClass(cat) boxlist.doBoxes() elif action=='drunk': cat.displayDrunks() else: print "Action {} is not implemented".format(action) sys.exit(1) return
Chunk referenced in 3.2

The doWine routine performs various management functions on the database. At the moment, the only such function is to add a new wine template to the database. The user is then prompted to fill out the fields for this wine before saving it to the database.

Other possible functions are to delete a wine, or move a wine record to the historical list (given, for example, that no further stocks of the wine are currently held). These functions are yet to be implemented.

3.2 define the editWine routine

<wine.py define the editWine routine 3.4> =
def editWine(cat,label): if cat.dict.has_key(label): wine=cat.dict[label] wine.request=requestString server=cat.server #print "2:server={}".format(server) print '<form action="http://{}/~ajh/cgi-bin/wineEdit.py">'.format(server) if debugFlag: print "<p>edit wine ={}</p>".format(wine) print wine.label,'<br/>' print wine.drunk,'<br/>' print "</form>" html=wine.toHTML() print html else: print "Wine '{}' is unknown".format(label) return
Chunk referenced in 3.2

3.3 define the displayRack routine

<wine.py define the displayRack routine 3.5> =
def displayRack(cat,action): # print table of wines in wine rack by location # first get wines in rack, indicated by location==(a|b|c|d|e|f|g|h|i|j)\d\d[-\d\d] server=cat.server rack={} rows=['a','b','c','d','e','f','g','h','i','j'] # now determine what section of rack if action=='true': rackStart=1; rackEnd=RackWidth elif action=='white': rackStart=1; rackEnd=8 elif action=='red': rackStart=9; rackEnd=20 elif action=='other': rackStart=21; rackEnd=RackWidth for row in rows: rack[row]=[None for i in range(RackWidth)] count=0 #print "got rack" for wine in cat.wines: #print "<p>{}</p>".format(wine) location=wine.stash for (qty,loc) in location: if not loc: loc='' res=re.match('({})(\d\d)(-(\d\d))?'.format(rows),loc) if res: row=res.group(1) #print row start=int(res.group(2)) endd=res.group(4) if endd: endd=int(endd) else: endd=start #print "start={}, end={}".format(start,endd) while start<=endd: target=rack[row][start-1] if target: print "<b>Two bottles in same location ({}{}): ".\ format(row,start-1), print "{} and {}</b>".format(target,wine.fields['contents']) rack[row][start-1]=wine start+=1 count+=1 # print links to other displays print wineModule.wineHeaders(server) #print rack print "<table border='1'>" print " <tr>" print " <th width='2%'>row</th>" for i in range(rackStart,rackEnd+1): col=rackEnd-i+rackStart print " <th width='5%'>{}</th>".format(col) print " <th width='2%'>row</th>" print " </tr>" revrows=rows revrows.reverse() thiscount=0 for r in revrows: row=rack[r] #print row print " <tr>" print " <td align='center'>{}</td>".format(r) for i in range(rackStart-1,rackEnd): col=rackEnd-1-i+rackStart-1 wine=row[col] bgcolor='white' if wine: winestr='<a href="wine.py?edit={}">{}</a>'.format(wine.label,wine.label) bgcolor=wine.type if bgcolor in ['red','white']: pass elif bgcolor=='other': bgcolor='paleyellow' elif bgcolor=='rose': bgcolor='pink' elif bgcolor=='sparkling': bgcolor='lightgreen' elif bgcolor=='dessert': bgcolor='yellow' elif bgcolor=='fortified': bgcolor='palepurple' else: bgcolor='olive' #print wine.type,wine thiscount+=1 else: winestr=' ' bgcolor='silver' print " <td bgcolor=\"{}\">{}</td>".format(bgcolor,winestr) print " <td align='center'>{}</td>".format(r) print " </tr>" print "</table>" print "<p>Rack contains {} bottles, {} in this section".format(count,thiscount) return
Chunk referenced in 3.2

3.4 The wine.py Main Program

<wine.py main program 3.6> =
try: host=os.environ["HOSTNAME"] except KeyError: cmd='/bin/hostname' pid=Popen(cmd,shell=True,stdout=PIPE,stderr=PIPE,close_fds=True) host=pid.communicate()[0].strip() try: server=os.environ["SERVER_NAME"] except KeyError: server='localhost' <wine.py determine the server and host names 3.7> <wine.py start the HTML output 3.8> <wine.py integrity and debug tests 3.9> <wine.py create and load the wine catalog 3.10> <wine.py collect invocation parameters 3.11> <wine.py process request string 3.12> <wine.py determine action to be performed 3.13>
Chunk referenced in 3.2

3.4.1 wine.py Determine the Server and Host Names

<wine.py determine the server and host names 3.7> =
# determine the server and host names # # the server is the address to which this request was directed, and is # useful in making decisions about what to render to the client. # Examples are "localhost", "www.ajh.id.au", "chairsabs.org.au". # # the host is the machine upon which the server is running, and may be # different from the server. This name is used to determine where to # store local data, such as logging information. For example, the # server may be "localhost", but this can run on a variety of hosts: # "murtoa", "dimboola", dyn-13-194-xx-xx", etc.. Incidentally, hosts # of the form "dyn-130-194-xx-xx" are mashed down to the generic "dyn". MacOSX='MacOSX' ; Solaris='Solaris' ; Linux="Linux" ; Ubuntu='Ubuntu' ostype=system=MacOSX # unless told otherwise if server in ["localhost","newport","spencer","hamilton","burnley"]: server="newport" else: sys.stderr.write("server/host values not recognized\n") sys.stderr.write("(supplied values are %s/%s)\n" % (server,host)) ostype=Linux system=Ubuntu sys.stderr.write("(assuming (ostype,system)=(%s,%s)\n" % (ostype,system))
Chunk referenced in 3.6 4.1

3.4.2 Determine the Server and Host Names

<wine.py start the HTML output 3.8> =
print "Content-type: text/html\n" #print "0:server={}".format(server) print ''' <!DOCTYPE html> <html> <head> <link rel="stylesheet" HREF="/~ajh/styles/wine.css" type="text/css" /> </head> <body> '''
Chunk referenced in 3.6

3.4.3 Integrity and Debug Tests

<wine.py integrity and debug tests 3.9> =
if server not in ['localhost','newport','hamilton','burnley']: print "<h3>THIS PAGE IS NOT AVAILABLE</h3>" sys.exit(1) if debugFlag: print "<p>NEW WINE PROGRAM!</p>\n" print "<p>*** server=%s ***</p>\n" % server print os.environ
Chunk referenced in 3.6

Ensure that this cgi script is only called from within the local network, on known servers.

3.4.4 create and load the wine catalog

<wine.py create and load the wine catalog 3.10> =
cat=wineModule.wineCatalog(server) cat.loadCatalog() cat.loadWineRack(server) if debugFlag: print "<p>catalog loaded<p>"
Chunk referenced in 3.6

Create and load the wine catalog

3.4.5 Collect Invocation Parameters

<wine.py collect invocation parameters 3.11> =
query_string='' # collect the original parameters from the redirect (if there is one!) if os.environ.has_key('QUERY_STRING'): query_string=os.environ['QUERY_STRING'] if debugFlag: print "<p>{}".format(query_string) form=cgi.parse_qs(query_string) if debugFlag: print "<p>{}".format(form) if form.has_key('debug') and form['debug'][0]=='true': sys.stderr.write("%s: %s\n" % (tsstring,repr(form))) debugFlag=True del form['debug'] # remove the debug flag to avoid confusion print "<h1>%s: INDEX.PY version %s</h1>\n" % (tsstring,version) print "<p>%s: os.environ=%s</p>\n" % (tsstring,repr(os.environ)) print "<p>%s: form=%s</p>\n" % (tsstring,repr(form)) sys.stderr.write("%s: redirect_query string=%s\n" % (tsstring,query_string)) else: form={} showOnlyPrevious=False if form.has_key('previous'): # display wines that have been completely consumed, as indicated # by a zero quantity in the holdings. This just sets a flag that # is passed into the HTML rendering routine (following). print "Showing only wines previously drunk and no longer stocked" showOnlyPrevious=True del form['previous'] remoteAdr='' if os.environ.has_key('REMOTE_ADDR'): remoteAdr=os.environ['REMOTE_ADDR']
Chunk referenced in 3.6

Get the parameters presented with this invocation. At this point, only the debug request is analysed and extracted. If the debug request is there (debug=true), the debugFlag is set, and the form entry removed, to avoid confusion with other potential parameters.

3.4.6 Process Request String

<wine.py process request string 3.12> =
requestString='' if os.environ.has_key('REQUEST_URI'): requestString=os.environ['REQUEST_URI'] if form.has_key('request'): requestString=form['request'][0] #print "requestString={}".format(requestString)
Chunk referenced in 3.6

We grab the request string so that if some subsidary action takes place (such as editing an entry), we can return to the original framework in which that subsidary action took place.

3.4.7 Determine Action to be Performed

<wine.py determine action to be performed 3.13> =
if form.has_key('action'): doWine(cat,form['action'][0]) sys.exit(0) if form.has_key('edit'): #print "3:server={}".format(server) editWine(cat,form['edit'][0]) sys.exit(0) if form.has_key('rack'): #displayRack(cat,form['rack'][0]) print cat.displayRack2(form['rack'][0]) sys.exit(0) sortval='vintage' if form.has_key('sort'): sortval=form.pop('sort')[0] if debugFlag: print "<p>Sorting by {}".format(sortval) filter=[] for k in form.keys(): filter.append((k,form[k])) if filter: (k,v)=filter[0] filter='{}={}'.format(k,v[0]) print wineModule.wineHeaders(server) print "<br>" html=cat.makeHTML(filter=filter,sortkey=sortval,requestString=requestString,showPrevious=showOnlyPrevious) print html print "</body>" print "</html>" now=datetime.datetime.now() tsstring=now.strftime("%Y%m%d:%H%M") sys.stderr.write("%s: [client %s] request satisfied\n" % (tsstring,remoteAdr)) sys.exit(0)
Chunk referenced in 3.6

4. The wineEdit Program

"wineEdit.py" 4.1 =
#!/usr/bin/python # imports import cgi import datetime import os #from personClass import Person,newPerson,setServer from wineModule import wineCatalog import re import sys debug=0 TREEHOME="/home/ajh/www/family/tree/persons" # determine which host/server environment host='newport' server=os.environ["SERVER_NAME"] #setServer(server) <wine.py determine the server and host names 3.7> remoteAddr=os.environ["REMOTE_ADDR"] remNet=re.sub('\.\d+$','',remoteAddr) timestamp=datetime.datetime.now().strftime('%Y%m%d-%H%M%S') #remoteEditor=os.environ["REMOTE_USER"] import cgitb cgitb.enable() print "Content-Type: text/html" # HTML is following print # blank line, end of headers print ''' <!DOCTYPE html> <html> <head> <link rel="stylesheet" HREF="/~ajh/styles/wine.css" type="text/css" /> </head> <body> ''' # get cgi parms form=cgi.FieldStorage() #print form if debug>0: print "<p>{}</p><p>{}</p>".format(os.environ,form) if form.has_key('request'): requestString=form['request'].value while len(requestString)>0 and requestString[0:1]=='/': requestString=requestString[1:] #print "requestString={}".format(requestString) update=form.has_key('save') # load wine catalog and get wine if debug: print "<H2>LOADING WINE CATALOG</H2>\n" cat=wineCatalog(server) cat.loadCatalog() label=form['save'].value if cat.dict.has_key(label): thiswine=cat.dict[label] else: print "Cannot identify wine labelled {}".format(label) sys.exit(1) # if there are updates, edit wine if debug: print "<p>update=%s" % (update) if update: if debug: print "<H2>UPDATING</H2>" thiswine.update(form) # display editable person if debug: print "<H2>EDITING</H2>\n" thiswine.request=requestString print thiswine.toHTML() # save wine in catalog if debug: print "<H2>SAVING</H2>\n" cat.saveCatalog() ## ## The End ##

This code is derived from a similar program for the family tree. Once we determine the context of invocation, and collect the parameters, it is simply a case of loading the wine catalog, collecting the desired wine, and generating the editing HTML. Once this is done, we exit (the saveCatalog is rather redundant, since nothing has changed yet).

5. TODOs

  1. move winerack into wineModule, and integrate handling of NoSlot wine placeholder.

6. The Makefile

Define your Makefile here.

"Makefile" 6.1 =
CGIBIN=${HOME}/public_html/cgi-bin GenFiles = install-wine include ${HOME}/etc/MakeXLP install-wine: wine.tangle chmod 755 wine.py cp wine.py ${CGIBIN} touch install-wine install-wineMod: wine.tangle chmod 755 wineModule.py cp wineModule.py ${CGIBIN} touch install-wineMod install-wineEd: wine.tangle chmod 755 wineEdit.py cp wineEdit.py ${CGIBIN} touch install-wineEd install: install-wine install-wineMod install-wineEd all: install clean: litclean -rm $(GenFiles)

7. Document History

20101029:093416 ajh 0.0 first draft, cast into the XLP template
20180828:105424 ajh 0.1 moved to lxml rather than ElementTree
20180830:182834 ajh 0.2 started to revise into better literate program representation
20180915:120310 ajh 0.3 clean up debugging outputs
20180915:175623 ajh 0.4 add Previous View link to displays
20180924:152221 ajh 0.4.1 suppress listing of wines out of stock
20180924:203008 ajh 0.4.2 generate separate listings of wines in stock versus out of stock
20181105:163914 ajh 0.4.3 add code to add a new wine to the database
20181107:151015 ajh 0.5.0 add display of wines drunk that have no further stocks
20190121:121651 ajh 0.5.1 use stash count to update quantity on saving
20190221:195610 ajh 0.5.2 general wine rack width, and extend to 29
20190601:162110 ajh 0.6.0 allow segmented views of wine rack
20190602:085648 ajh 0.6.1 add links to wine rack sections
20190603:144923 ajh 0.6.2 move header generation to separate procedure
20190603:154807 ajh 0.6.3 add colour backgrounds to wine rack
20190610:145933 ajh 0.6.4 reinsert add new wine link
20190701:184543 ajh 0.7.0 significant restructure to wineModule, adding wineRackClass and new displayRack function
20190710:211539 ajh 0.8.0 add box location to database, load and save Catalog, and displayBoxes operation.
20190711:134057 ajh 0.8.1 refine loadCatalog algorithm to use lxml iterations
20190711:134915 ajh 0.8.2 move box listing to WineBoxClass
20191021:102832 ajh 0.8.3 ensure server parameter passed to wineHeaders
<version 7.1.1> = 0.8.3
<date 7.1.2> = 20191021:102832

8. Indices

8.1 Files

File Name Defined in
Makefile 6.1
wine.py 3.1, 3.2
wineEdit.py 4.1
wineModule.py 2.1

8.2 Chunks

Chunk Name Defined in Used in
add wine labelled label to catalog 2.18 2.16
date 7.1.2
define the displayDrunks routine 2.26 2.15
define the displayRack2 routine 2.25 2.15
define the loadCatalog routine 2.16 2.15
define the loadWineRack routine 2.28 2.15
define the makeHTML routine 2.21, 2.22, 2.23 2.15
define the nextwine generator 2.27
define the saveCatalog routine 2.20 2.15
define the wine box class 2.30 2.1
define the wine catalog class 2.15 2.1
define the wine class 2.4 2.1
define the wine rack class 2.29 2.1
define the wineClass initialization 2.5 2.4
define the wineClass string representation 2.6 2.4
define the wineClass toHTML function 2.7 2.4
define the wineClass update procedure 2.12 2.4
generate toHTML form for line 1 (LABEL) 2.8 2.7
generate toHTML form for line 2 (VINTAGE,QUANTITY,STASHES) 2.9 2.7
generate toHTML form for line 3 (COST,TYPE,VARIETY) 2.10 2.7
generate toHTML form for lines 4 et seq (DRINKING) 2.11 2.7
get box details from database 2.19 2.16
get details for this wine entry 2.17 2.16
imports 2.2 2.1
makeHTML process a single wine label 2.24 2.23
version 7.1.1
wine.py collect invocation parameters 3.11 3.6
wine.py create and load the wine catalog 3.10 3.6
wine.py define the displayRack routine 3.5 3.2
wine.py define the doWine routine 3.3 3.2
wine.py define the editWine routine 3.4 3.2
wine.py define the wineHeaders routine 2.3 2.1
wine.py determine action to be performed 3.13 3.6
wine.py determine the server and host names 3.7 3.6, 4.1
wine.py integrity and debug tests 3.9 3.6
wine.py main program 3.6 3.2
wine.py process request string 3.12 3.6
wine.py start the HTML output 3.8 3.6
wineClass Update each field 2.13 2.12
wineClass Update the drunk fields 2.14 2.12

8.3 Identifiers

Identifier Defined in Used in
makeHTML 2.21 3.13
toHTML 2.7 3.4, 4.1
wineCatalog 2.15
wineClass 2.4

18 accesses since 16 Jan 2019, HTML cache rendered at 20191021:1406