XLP Literate Program Template
A.J.Hurst
Version 0.8.4
20200817:133040
Table of Contents
1. Introduction
This program suite provides an interactive web-based interface
to the wine database.
There are several programs in this suite:
-
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).
-
The wine web program wine.py. This program is called
from web pages, and renders various views of the wine
database.
-
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 =
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
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
2.3 Define the Wine Class
<define the wine class 2.4> =
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
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
- label
-
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.
- vintage
-
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.
- quantity
-
The total number of bottles held in stock. This must be
consistent with the various stash amounts (see below).
It must be an integer.
- type
-
The type of the wine, whether sparking, red, white,
fortified, etc.. A text string.
- variety
-
The grape variety used in making the wine. A text string.
- company
-
The wine making company. A text string. This value is
not currently used, and will be added to the database at
a future date.
- cost
-
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.
- stash
-
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.
- drunk
-
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
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> =
- {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.
2.3.3.1 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"
2.3.3.2 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"
2.3.3.3 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"
2.3.3.4 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"
2.3.4 Define the wineClass Update Procedure
<define the wineClass update procedure 2.12> =
2.3.4.1 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"
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.
2.3.4.2 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
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> =
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> =
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.
2.4.1.1 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
2.4.1.2 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)
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.
2.4.1.3 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
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()
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=''
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
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
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
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
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
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
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"
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
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:
- RackWidth
- the width of the rack array
- RackHeight
- the height of the rack array
- WineArray
-
the rectangular array itself, with elements of type
wineClass.
- NoSlot
-
an object of type wineClass indicating no bottle
can be stored in this location. This allows
non-rectangular subsets of the WineArray.
- initWineArray
-
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
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.
- WineBoxes
-
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.quantity,'</td>'
print '</tr>'
print '</table>'
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)
"wine.py" 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
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
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
def columnNumbers():
'''print column numbers across the page'''
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>"
# 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'>"
columnNumbers()
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>"
columnNumbers()
print "</table>"
print "<p>Rack contains {} bottles, {} in this section".format(count,thiscount)
return
3.4 The wine.py Main Program
<wine.py main program 3.6> =
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))
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>
'''
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
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>"
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']
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)
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)
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
-
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 |
20200817:133040 |
ajh |
0.8.4 |
revise layout of box listing |
<version 7.1.1> = 0.8.4
<date 7.1.2> = 20200817:133040
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