--- /dev/null
+Web, python, and command-line interfaces for managing a chemical inventory.
+
+COMMANDS
+
+The web interface (chem_web.py) is a web-daemon using web.py and it's build in htmlserver.
+Standard command for starting the daemon
+ $ chem_web.py -a 192.168.1.2:55555
+Standard command for stopping the daemon
+ $ chem_web.py --stop
+
+From the command line, you can validate CAS#s (and possibly other fields) with
+ $ python chem_db.py -f current/inventory.db -V
+
+
+DATABASE FORMAT
+
+text_db.py provides the python interface to this format. The basic
+idea was to produce and use files which were M$-Excel-compatible,
+since future users might not want to maintain this interface. A brief
+example inventory.db file is included in the examples directory.
+
+Tab-delimited ('\t') fields
+Endline-delimited ('\n') records
+The first line is the header (starts with a pound sign '#').
+The header should be a tab-delimited list of short field names.
+If the second line also begins with a pound sign,
+it contains a tab-delimiteded list of long field names.
+Blank lines are ignored.
+
+The fields H, F, R, and S are the NFPA Health, Fire, Reactivity, and Special Hazards (NFPA diamond).
+
+Blue: Health Hazard
+ 0 Hazard no greater than ordinary material
+ 1 May cause irritation; minimal residual injury
+ 2 Intense or prolonged exposure may cause incapacitation;
+ Residual injury may occur if not treated
+ 3 Exposure could cause serious injury even if treated
+ 4 Exposure may cause death
+Red: Fire Hazard
+ 0 Will not burn
+ 1 Must be preheated for ignition; flashpoint above 200°F (93°C)
+ 2 Must be moderately heated for ignition, flashpoint above 100°F (38°C)
+ 3 Ignition may occur under most ambient conditions,
+ Flashpoint below 100°F (38°C)
+ 4 Extremely flammable and will readily disperse through air under standard
+ conditions, flashpoint below 73°F (23°C)
+Reactivity hazards:
+ 0 Stable
+ 1 May become unstable at elevated temperatures and pressures.
+ May be mildly water reactive
+ 2 Unstable; may undergo violent decomposition, but will not detonate.
+ May form explosive mixtures with water
+ 3 Detonates with strong ignition source
+ 4 Readily detonates
+Special Hazards have the following codes:
+ OX strong oxidizer
+ -W- water reactive
+ SA simple ascphyxiants
+ (The only gases for which this symbol is permitted are nitrogen, helium,
+ neon, argon, krypton, and xenon..)
+Non-official Special Hazard codes:
+ ACID acid
+ ALK base
+ COR corrosive
+ BIO Biohazard
+ POI Poison
+ CRY Cyrogenic
+
+
+CONTENTS
+
+README this file
+text_db.py python interface to the above db format
+chem_db.py chem-inventory specific functionality such as
+ * CAS # validation,
+ * a Material Saftey Data Sheet (MSDS) manager,
+ * document-generation drivers, and
+ * a simple command line interface.
+current store the current database file.
+backup store previous (timestamped) database files.
+ You may want to clean this out occasionally.
+MSDS store Material Saftey Data Sheets by database index number
+docs latex (and metapost) sources for document generation.
+chem_web.py daemonized web bindings to chem_web.py
+ssl Secure Socket Layer (SSL) key and certificate generation tests.
+
+
+GENERATED FILES AND DIRECTORIES
+
+templates quasi-HTML templates for pages generated chem_web.py
+chem_web.pid store the Process ID (PID) for a daemonized chem_web.py process
+chem_web.log log chem_web activity (maintained between runs, so remove periodically)
+
+
+TODO
+
+Security:
+I can create my own self-signed certificate.
+Protects against eavesdropping, but not man-in-the-middle attacks.
+Solution for now: hide from external world, trust everyone inside, backup before any change.
+
+
+Print columns in a particular order from the database with
+ $ awk -F ' ' '{print $1, "\t", $9, "\t", , "\t", $2}' current/inventory.db | less
--- /dev/null
+#
+# certgen.py
+#
+# Copyright (C) Martin Sjogren and AB Strakt 2001, All rights reserved
+#
+# $Id: certgen.py,v 1.2 2004/07/22 12:01:25 martin Exp $
+#
+"""
+Certificate generation module.
+"""
+
+from OpenSSL import crypto
+
+TYPE_RSA = crypto.TYPE_RSA
+TYPE_DSA = crypto.TYPE_DSA
+
+def createKeyPair(type, bits):
+ """
+ Create a public/private key pair.
+
+ Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA
+ bits - Number of bits to use in the key
+ Returns: The public/private key pair in a PKey object
+ """
+ pkey = crypto.PKey()
+ pkey.generate_key(type, bits)
+ return pkey
+
+def createCertRequest(pkey, digest="md5", **name):
+ """
+ Create a certificate request.
+
+ Arguments: pkey - The key to associate with the request
+ digest - Digestion method to use for signing, default is md5
+ **name - The name of the subject of the request, possible
+ arguments are:
+ C - Country name
+ ST - State or province name
+ L - Locality name
+ O - Organization name
+ OU - Organizational unit name
+ CN - Common name
+ emailAddress - E-mail address
+ Returns: The certificate request in an X509Req object
+ """
+ req = crypto.X509Req()
+ subj = req.get_subject()
+
+ for (key,value) in name.items():
+ setattr(subj, key, value)
+
+ req.set_pubkey(pkey)
+ req.sign(pkey, digest)
+ return req
+
+def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter), digest="md5"):
+ """
+ Generate a certificate given a certificate request.
+
+ Arguments: req - Certificate reqeust to use
+ issuerCert - The certificate of the issuer
+ issuerKey - The private key of the issuer
+ serial - Serial number for the certificate
+ notBefore - Timestamp (relative to now) when the certificate
+ starts being valid
+ notAfter - Timestamp (relative to now) when the certificate
+ stops being valid
+ digest - Digest method to use for signing, default is md5
+ Returns: The signed certificate in an X509 object
+ """
+ cert = crypto.X509()
+ cert.set_serial_number(serial)
+ cert.gmtime_adj_notBefore(notBefore)
+ cert.gmtime_adj_notAfter(notAfter)
+ cert.set_issuer(issuerCert.get_subject())
+ cert.set_subject(req.get_subject())
+ cert.set_pubkey(req.get_pubkey())
+ cert.sign(issuerKey, digest)
+ return cert
+
--- /dev/null
+#!/usr/bin/python
+
+"""
+Extend text_db with a CAS# validator, a command line interface, and document generation.
+"""
+
+from text_db import *
+import re
+import os
+import os.path
+import time
+
+def valid_CASno(cas_string, debug=False):
+ """
+ Check N..NN-NN-N format, and the checksum digit for valid CAS number structure.
+ see http://www.cas.org/expertise/cascontent/registry/checkdig.html
+ for N_n .. N_4 N_3 - N_2 N_1 - R
+ R = remainder([sum_{i=1}^n i N_i ] / 10 )
+ Ignores 'na' and '+secret-non-hazardous'
+ >>> valid_CASno('107-07-3')
+ True
+ >>> valid_CASno('107-08-3')
+ False
+ >>> valid_CASno('107-083')
+ False
+ """
+ for string in ['na', '+secret-non-hazardous']:
+ # the first marks a non-existent CAS#
+ # the last marks items with secret, non-hazardous ingredients for which we have no CAS#
+ if cas_string == string:
+ return True
+ # check format,
+ # ^ matches the start of the string
+ # \Z matches the end of the string
+ regexp = re.compile('^[0-9]{2,}[-][0-9]{2}[-][0-9]\Z')
+ if regexp.match(cas_string) == None:
+ if debug : print >> stderr, "invalid CAS# format: '%s'" % cas_string
+ return False
+ # generate check digit
+ casdigs = "".join(cas_string.split('-')) # remove '-'
+ sumdigs = list(casdigs[:-1])
+ sumdigs.reverse()
+ sum=0
+ for i in range(len(sumdigs)) :
+ sum += (i+1)*int(sumdigs[i])
+ check = sum % 10
+ if int(casdigs[-1]) == check :
+ return True
+ else :
+ if debug : print >> stderr, "invalid CAS# check: '%s' (expected %d)" % (cas_string, check)
+ return False
+
+class MSDS_manager (object) :
+ """
+ Manage Material Saftey Data Sheets (MSDSs)
+ """
+ def __init__(self, db, dir="./MSDS/") :
+ self.db = db
+ self.dir = dir
+ self.MIMEs = ['application/pdf',
+ 'text/html',
+ 'text/plain']
+ self.MIME_exts = ['pdf', 'html', 'txt']
+ self.check_dir()
+ def check_dir(self) :
+ "Create the MSDS directory if it's missing"
+ if os.path.isdir(self.dir) :
+ return # all set to go
+ elif os.path.exists(self.dir) :
+ raise Exception, "Error: a non-directory file exists at %s" % self.dir
+ else :
+ os.mkdir(self.dir)
+ def basepath(self, id) :
+ assert type(id) == type(1), 'id must be an integer, not %s (%s)' \
+ % (type(id), str(id))
+ return os.path.join(self.dir, "%d" % id)
+ def local_basepath(self, id) : # for symbolic links
+ assert type(id) == type(1), 'id must be an integer, not %s (%s)' \
+ % (type(id), str(id))
+ return "./%d" % id
+ def MIME_ext(self, mime) :
+ assert mime in self.MIMEs, \
+ "invalid MIME type '%s'\nshould be one of %s" % (mime, self.MIMEs)
+ i = self.MIMEs.index(mime)
+ ext = self.MIME_exts[i]
+ return ext
+ def path(self, id, mime) :
+ return "%s.%s" % (self.basepath(id), self.MIME_ext(mime))
+ def local_path(self, id, mime) :
+ return "%s.%s" % (self.local_basepath(id), self.MIME_ext(mime))
+ def save(self, id, filetext, mime='application/pdf') :
+ "Save the binary byte string FILE to the path for ID"
+ print >> file(self.path(id, mime), 'wb'), filetext,
+ def link(self, id, target_id) :
+ # target_id already exists, create a symlink to it for id.
+ target_mime = self.get_MSDS_MIME(target_id)
+ target_path = self.local_path(target_id, target_mime)
+ path = self.path(id, target_mime)
+ #os.link(self.path(target_id), self.path(id)) # hard link...
+ os.symlink(target_path, path) # ... or soft link
+ def has_MSDS_MIME(self, id, mime) :
+ """
+ >>> m = MSDS_manager(db=None)
+ >>> print m.has_MSDS_type(102, 'pdf') # test on html
+ False
+ >>> print m.has_MSDS_type(102, 'html') # test on html
+ True
+ >>> print m.has_MSDS_type(6, 'pdf') # test on pdf symlink
+ True
+ """
+ return os.path.exists(self.path(id, mime))
+ def get_MSDS_path(self, id) :
+ """
+ >>> m = MSDS_manager(db=None)
+ >>> print m.get_MSDS_path(102) # test on html
+ ./MSDS/102.html
+ >>> print m.get_MSDS_path(1) # test on pdf
+ ./MSDS/1.pdf
+ >>> print m.get_MSDS_path(6) # test on pdf symlink
+ ./MSDS/6.pdf
+ """
+ for mime in self.MIMEs :
+ if self.has_MSDS_MIME(id, mime) :
+ return self.path(id, mime)
+ return None
+ def get_MSDS_MIME(self, id) :
+ """
+ >>> m = MSDS_manager(db=None)
+ >>> print m.get_MSDS_MIME(102) # test on html
+ text/html
+ >>> print m.get_MSDS_MIME(1) # test on pdf
+ application/pdf
+ >>> print m.get_MSDS_MIME(6) # test on pdf symlink
+ application/pdf
+ """
+ for mime in self.MIMEs :
+ if self.has_MSDS_MIME(id, mime) :
+ return mime
+ return None
+ def has_MSDS(self, id) :
+ if self.get_MSDS_path(id) == None :
+ return False
+ return True
+ def get_all(self, simlinks=True) :
+ ret = []
+ for record in self.db.records() :
+ p = self.get_MSDS_path( int(record['ID']) )
+ if p != None :
+ if simlinks == False and os.path.islink( p ) :
+ continue # ignore the symbolic link
+ ret.append({'ID':record['ID'], 'Name':record['Name']})
+ return ret
+
+class docgen (object) :
+ "Generate the officially required documents"
+ def __init__(self, db) :
+ self.db = db
+ def _latex_safe(self, string):
+ string = string.replace('%', '\%')
+ string = string.replace('>', '$>$')
+ string = string.replace('<', '$<$')
+ return string
+ def _set_main_target(self, target):
+ print >> file('./docs/main.tex', 'w'), \
+ """\documentclass[letterpaper]{article}
+
+\input{%s}
+""" % target
+ def _make_pdf(self, target_file):
+ os.system('cd ./docs && make pdf')
+ path = os.path.join('./docs/', target_file)
+ os.system('cp ./docs/main.pdf %s' % path)
+ return path
+ def inventory(self, namewidth='a') :
+ pp = db_pretty_printer(self.db)
+ active_ids = []
+ for record in self.db.records() :
+ if record['Disposed'] == '' : # get ids for chemicals we still have
+ active_ids.append(record['db_id'])
+ active_fields = ['ID', 'Name', 'Amount',
+ 'H', 'F', 'R', 'O', 'M', 'C', 'T']
+ width = {}
+ for field in active_fields :
+ width[field] = 'a'
+ width['Name'] = namewidth
+ ## Plain text method
+ #string = "Chemical inventory:\t\tGenerated on %s\n\n" \
+ # % time.strftime('%Y-%m-%d')
+ #string += pp.multi_record_string(active_ids, active_fields,
+ # width=width, FS=' ')
+ # return string
+ ## Latex method
+ string = "\\begin{longtable}{l l l c c c c c c c}\n"
+ string += ('%% The header for the remaining page(s) of the table...\n'
+ 'ID & Name & Amount & H & F & R & O & M & C & T \\\\\n'
+ '\\hline\n'
+ '\\endhead\n')
+ for db_id in active_ids :
+ record = self.db.record(db_id)
+ string += " %s & %s & %s & %s & %s & %s & %s & %s & %s & %s \\\\\n" \
+ % (self._latex_safe(record['ID']),
+ self._latex_safe(record['Name']),
+ self._latex_safe(record['Amount']),
+ self._latex_safe(record['H']),
+ self._latex_safe(record['F']),
+ self._latex_safe(record['R']),
+ self._latex_safe(record['O']),
+ self._latex_safe(record['M']),
+ self._latex_safe(record['C']),
+ self._latex_safe(record['T']))
+ string += "\\end{longtable}\n"
+ print >> file('./docs/inventory_data.tex', 'w'), string
+ ## alter main.tex to point to the inventory template.
+ self._set_main_target('inventory_template')
+ ## run latex
+ path = self._make_pdf('inventory.pdf')
+ return path
+ def door_warning(self) :
+ pp = db_pretty_printer(self.db)
+ all_ids = range(self.db.len_records())
+
+ # Search the database to find the nasties
+ NFPA_maxs = {'H':0, 'F':0, 'R':0, 'O':[]}
+ Mutagens = []
+ Carcinogens = []
+ Teratogens = []
+ Healths = []
+ Fires = []
+ Reactivities = []
+ Others = []
+ for record in self.db.records() :
+ if record['Disposed'] == '' : # chemicals we still have
+ for field in ['H', 'F', 'R', 'O'] :
+ r = record[field]
+ if r != '' and r != '?' :
+ if field != 'O' and int(r) > NFPA_maxs[field] :
+ NFPA_maxs[field] = int(r)
+ elif field == 'O' and not r in NFPA_maxs['O'] :
+ NFPA_maxs[field].append(r)
+ for field,array in zip(['M','C','T'],
+ [Mutagens,
+ Carcinogens,
+ Teratogens]) :
+ if record[field] != '' and record[field] != '?':
+ array.append(record['db_id'])
+ # now that we've found the max NFPAs,
+ # find all the chemicals at those levels
+ for record in self.db.records() :
+ if record['Disposed'] == '' : # chemicals we still have
+ for field,array in zip(['H', 'F', 'R', 'O'],
+ [Healths, Fires,
+ Reactivities, Others]) :
+ r = record[field]
+ if r != '' and r != '?' :
+ if field != 'O' and int(r) == NFPA_maxs[field] :
+ array.append(record['db_id'])
+ elif field == 'O' and r in NFPA_maxs['O'] :
+ array.append(record['db_id'])
+
+ ## generate the output
+ # first, update the NFPA grapic code
+ if 'OX' in NFPA_maxs['O'] : OX = 'y'
+ else : OX = 'n'
+ if 'W' in NFPA_maxs['O'] : W = 'y'
+ else : W = 'n'
+ os.system('./docs/mp/gen_NFPA.sh %d %d %d %s %s > ./docs/mp/NFPA.mp'
+ % (NFPA_maxs['H'], NFPA_maxs['F'], NFPA_maxs['R'], OX, W))
+ # now generate a list of the nasties ( Amount & ID & Name )
+ string = "\\begin{tabular}{r r l}\n"
+ for field,name,array in zip(['H', 'F', 'R', 'O'],
+ ['Health', 'Fire',
+ 'Reactivity', 'Other'],
+ [Healths, Fires,
+ Reactivities, Others]) :
+ string += " \multicolumn{3}{c}{\Tstrut %s : %s} \\\\\n" \
+ % (name, NFPA_maxs[field])
+ for db_id in array :
+ record = self.db.record(db_id)
+ string += " %s & %s & %s \\\\\n" \
+ % (self._latex_safe(record['Amount']),
+ self._latex_safe(record['ID']),
+ self._latex_safe(record['Name']))
+ if len(array) == 0 :
+ string += " \multicolumn{3}{c}{ --- } \\\\\n"
+ for hazard,array in zip(['Mutagens','Carcinogens','Teratogens'],
+ [Mutagens, Carcinogens, Teratogens]) :
+ string += " \multicolumn{3}{c}{\Tstrut %s} \\\\\n" % (hazard)
+ for db_id in array :
+ record = self.db.record(db_id)
+ string += " %s & %s & %s \\\\\n" \
+ % (self._latex_safe(record['Amount']),
+ self._latex_safe(record['ID']),
+ self._latex_safe(record['Name']))
+ if len(array) == 0 :
+ string += " \multicolumn{3}{c}{ --- } \\\\\n"
+ string += "\\end{tabular}\n"
+ print >> file('./docs/door_data.tex', 'w'), string
+ ## alter main.tex to point to the door template.
+ self._set_main_target('door_template')
+ ## run latex
+ path = self._make_pdf('door_warning.pdf')
+ return path
+
+def _test():
+ import doctest
+ doctest.testmod()
+
+def open_IOfiles(ifilename=None, ofilename=None, debug=False):
+ if ifilename :
+ if debug : print >> stderr, "open input file '%s'" % ifilename
+ ifile = file(ifilename, 'r')
+ else :
+ ifile = stdin
+ if ofilename :
+ if debug : print >> stderr, "open output file '%s'" % ofilename
+ ofile = file(ofilename, 'w')
+ else :
+ ofile = stdout
+ return (ifile, ofile)
+
+def close_IOfiles(ifilename=None, ifile=stdin,
+ ofilename=None, ofile=stdout,
+ debug=False):
+ if ifilename :
+ if debug : print >> stderr, "close input file '%s'" % ifilename
+ ifile.close()
+ if ofilename :
+ if debug : print >> stderr, "close output file '%s'" % ofilename
+ ofile.close()
+
+
+if __name__ == "__main__" :
+ from optparse import OptionParser
+
+ parser = OptionParser(usage="usage: %prog [options]", version="%prog 0.1")
+
+ parser.add_option('-f', '--input-file', dest="ifilename",
+ help="Read input from FILE (default stdin)",
+ type='string', metavar="FILE")
+ parser.add_option('-o', '--output-file', dest="ofilename",
+ help="Write output to FILE (default stdout)",
+ type='string', metavar="FILE")
+ parser.add_option('-d', '--delimiter', dest="FS", # field seperator
+ help="Set field delimiter (default '%default')",
+ type='string', metavar="DELIM", default='\t')
+ parser.add_option('-p', '--print-fields', dest="print_fields",
+ help="Only print certain fields (e.g. 0,3,4,2)",
+ type='string', metavar="FIELDS")
+ parser.add_option('-r', '--print-records', dest="print_records",
+ help="Only print certain records (e.g. 0:3)",
+ type='string', metavar="RECORDS")
+ parser.add_option('-w', '--column-width', dest="width",
+ help="Set column width for short-format output.",
+ type='string', metavar="WIDTH")
+ parser.add_option('-L', '--long-format', dest="long_format",
+ help="Print long format (several lines per record)",
+ action='store_true', default=False)
+ parser.add_option('-l', '--short-format', dest="long_format",
+ help="Print short format (default) (one lines per record)",
+ action='store_false', default=False)
+ parser.add_option('-t', '--test', dest="test",
+ help="Run docutils tests on db.py",
+ action="store_true", default=False)
+ parser.add_option('-V', '--validate', dest="validate",
+ help="Validate CAS#s (no other output)",
+ action="store_true", default=False)
+ parser.add_option('-v', '--verbose', dest="verbose",
+ help="Print lots of debugging information",
+ action="store_true", default=False)
+
+ (options, args) = parser.parse_args()
+ parser.destroy()
+
+ ifile,ofile = open_IOfiles(options.ifilename, options.ofilename,
+ options.verbose)
+
+ if options.test :
+ _test()
+ elif options.validate :
+ db = text_db(filename=None)
+ pp = db_pretty_printer(db)
+
+ # read in and parse the file
+ db._parse(ifile.read())
+
+ CAS_DELIM = ',' # seperate CAS entries for chemicals with multiple CAS numbers
+ PERCENT_DELIM = ':' # seperate CAS number from ingredient percentage
+ for record in db.records() :
+ valid = True
+ cas = record['CAS#']
+ if len(cas.split(CAS_DELIM)) == 0 : # cas = 'N...N-NN-N'
+ if not valid_CASno(cas, options.verbose) :
+ valid = False
+ print >> ofile, "Invalid CAS# in record: '%s'" % cas
+ else : # cas = 'N...N-NN-N:X%,N...N-NN-N:Y%,...'
+ for casterm in cas.split(CAS_DELIM) : # casterm = 'N...N-NN-N:X%'
+ c = casterm.split(PERCENT_DELIM)[0] # c = 'N...N-NN-N'
+ if not valid_CASno(c, options.verbose) :
+ valid = False
+ print >> ofile, "Invalid CAS* in record: '%s'" % c
+ if not valid :
+ print >> ofile, "in record %s: %s" % (record['ID'], record['Name'])
+ #pp.full_record_string(record)
+
+ else :
+ db = text_db(filename=None)
+
+ # read in and parse the file
+ db._parse(ifile.read())
+ pp = db_pretty_printer(db)
+ if options.long_format :
+ for id in pp._norm_record_ids(options.print_records) :
+ string = pp.full_record_string_id(id)
+ else :
+ # pythonize the width option
+ if (options.width != None
+ and options.width != 'a'
+ and len(options.width.split(':')) == 1
+ ) :
+ width = int(options.width)
+ elif len(options.width.split(':')) > 1 :
+ width = {}
+ for kv in options.width.split(',') :
+ spl = kv.split(':')
+ assert len(spl) == 2, 'invalid width "%s" in "%s"' % (kv, options.width)
+ if spl[1] == 'a' :
+ width[spl[0]] = spl[1]
+ else :
+ width[spl[0]] = int(spl[1])
+
+ string = pp.multi_record_string(options.print_records,
+ options.print_fields,
+ width,
+ options.FS)
+ print >> ofile, string,
+
+ close_IOfiles(options.ifilename, ifile,
+ options.ofilename, ofile, options.verbose)
--- /dev/null
+#!/usr/bin/python
+#
+# Web application for posting additions/modifications to the chem-inventory
+# derived from simplewiki and wiki_py, by Adam Bachman
+# ( http://bachman.infogami.com/ )
+# Authentication following Snakelets
+# http://snakelets.sourceforge.net/manual/authorization.html
+
+# Remember to delete the templates directory after making alterations,
+# because this program only generates templates if they are missing.
+
+
+## Bring in some useful goodies:
+# web, because we're running a web server, and generating html;
+# time, for timestamping backups;
+# os, for creating the template directories; and
+# re, for replacing internal node links with proper markup
+import web, os, re
+# md5, for hashing passwords
+#import md5
+
+# database must support the following methods :
+# field_list() : return an ordered list of available fields (fields unique)
+# long_field() : return an dict of long field names (keyed by field names)
+# record(id) : return a record dict (keyed by field names)
+# records() : return an ordered list of available records.
+# backup() : save a copy of the current database somehow.
+# set_record(i, newvals) : set a record by overwriting any preexisting data
+# : with data from the field-name-keyed dict NEWVALS
+import text_db
+database = text_db.text_db
+from chem_db import valid_CASno, MSDS_manager, docgen
+DB_FILE = 'inventory.db'
+import sys, daemon, logging
+PID_FILE = './chem_web.pid'
+LOG_FILE = './chem_web.log'
+
+## Also import a markup language, and associated regexp functions
+from markdown import markdown
+## for converting the first character in record names, and stripping |
+from string import upper, rstrip
+
+## for SSL
+SSL = False
+if SSL :
+ from mk_simple_certs import get_cert_filenames
+ server_name = 'chem_web'
+
+__version__ = "0.2"
+
+## For web.py, when a URL matches '/view', handle with class 'view'
+# when it matches '/edit', handle with class 'edit',
+# and when it matches '/login', handle with class 'login',
+# Note, there are no .html extension.
+#
+# link to edit pages using db_id
+# link to MSDSs with ID
+urls = ('/(view)?', 'view',
+ '/edit/([0-9]+)', 'edit',
+ '/MSDS/([0-9]+)', 'MSDS',
+ '/docs/([a-z._]*)', 'docs',
+ '/login', 'login')
+
+# limit the display width of selected fields
+WIDTH_MAX = {'CAS#':12}
+
+# set the display width of entry fields
+ENTRY_WIDTH = 50
+
+class view (object) :
+ "Print the database"
+ # when the server recieves the command "GET '/view', call view.get('view')
+ def GET(self, name):
+ """
+ Render the view.html template using name and the previous text.
+ If the file doesn't exist, use default text '%s doesn't exist'.
+ """
+ if name == None :
+ name = 'view'
+ db._refresh()
+ recs = []
+ for record in db.records() :
+ id = int(record['ID'])
+ record['ID'] = '<a href="/edit/%d">%s</a>' \
+ % (id, record['ID'])
+ if MSDSman.has_MSDS(id) : # link the id to the MSDS
+ record['Name'] = '<a href="/MSDS/%d">%s</a>' \
+ % (record['db_id'], record['Name'])
+ recs.append(record)
+ print render.view('Chemical inventory',
+ db.len_records(), db.field_list(), recs)
+
+class edit (object) :
+ "Provide methods for handling requests related to edit pages"
+ def GET(self, name):
+ """
+ Render the edit.html template using the specified record number.
+ """
+ db._refresh()
+ db_id = int(name)
+ if db_id >= db.len_records() :
+ logging.info("new record")
+ assert db_id == db.len_records(), \
+ 'invalid id: %d (max %d)' % (db_id, db.len_records())
+ record = db.new_record()
+ record['ID'] = str(record['db_id'])
+ db.set_record(db_id, record)
+ if MSDSman.has_MSDS(db_id) :
+ MSDSs = None
+ else :
+ # only ask for an MSDS if we still need one
+ MSDSs = MSDSman.get_all(simlinks=False)
+ print render.edit(int(name), db.field_list(),
+ db.long_fields(), db.record(int(name)), MSDSs)
+ def POST(self, name):
+ """
+ Read the form input and update the database accordingly.
+ Then redirect to /view.
+ """
+ db._refresh()
+ record = db.record(int(name))
+ ## Generate a new record from the form input
+ # 'MSDS={}' to storify MSDS as a FieldStorage (dict-like) instance,
+ # otherwise it's saved as a string, and we loose info about it.
+ # e.g. newvals['MSDS'].type = 'text/html'
+ # The contents of the file are saved to newvals['MSDS'].value
+ newvals = web.input(MSDS={})
+ update = False
+ for field in db.field_list() :
+ if newvals[field] != record[field] :
+ update=True
+ record[field] = newvals[field]
+ if 'MSDS source' in newvals :
+ # Handle any MSDS file actions
+ if newvals['MSDS source'] == 'upload' :
+ if len(newvals['MSDS'].filename) > 0 and len(newvals['MSDS'].value) > 0:
+ # only save if there is a file there to save ;)
+ #print >> stderr, web.ctx.env['CONTENT_TYPE']
+ #print >> stderr, web.ctx.keys()
+ #print >> stderr, dir(newvals['MSDS'])
+ #print >> stderr, type(newvals['MSDS'])
+ #print >> stderr, newvals['MSDS'].filename
+ #print >> stderr, newvals['MSDS'].type
+ MSDSman.save(int(name), newvals['MSDS'].value,
+ newvals['MSDS'].type)
+ else :
+ logging.info('linking MSDS %d to %d' \
+ % (int(record['ID']),
+ int(newvals['MSDS share'])) )
+ MSDSman.link(int(record['ID']),
+ int(newvals['MSDS share']))
+ if update :
+ db.set_record(int(name), record, backup=True)
+ # redirect to view all
+ web.seeother('/view')
+
+class MSDS (object) :
+ "Serve MSDS files by ID"
+ def GET(self, name):
+ "Serve MSDS files by ID"
+ id = int(name)
+ if MSDSman.has_MSDS(id) :
+ mime = MSDSman.get_MSDS_MIME(id)
+ web.header("Content-Type", mime)
+ print file(MSDSman.get_MSDS_path(id), 'rb').read() ,
+ else :
+ print render.error(id)
+
+class docs (object) :
+ "Generate and serve assorted official documents"
+ def GET(self, name):
+ """
+ List the available documents.
+ """
+ db._refresh()
+ if name == '' :
+ print render.docs()
+ if name == 'inventory.pdf' :
+ path = dgen.inventory(namewidth=40)
+ print file(path, 'rb').read() ,
+ if name == 'door_warning.pdf' :
+ path = dgen.door_warning()
+ print file(path, 'rb').read() ,
+
+
+class login (object) :
+ "Print an alphabetized index of all existing pages"
+ def GET(self):
+ print "Not yet implemented"
+ #print render.stat('login')
+
+class markup (object) :
+ "Convert text to html, using Markdown with a bit of preformatting"
+ def __init__(self) :
+ # [[optional display text|name with or_without_spaces]]
+ linkregexp = ('\[\['
+ +'([^\|\[\]]*\|)?'
+ +'([a-zA-Z0-9-_ ]*)'
+ +'\]\]')
+ #print linkregexp
+ self.linkexp = re.compile(linkregexp)
+ self.linebreak = ' \n'
+ self.uscore = re.compile('_')
+ self.space = re.compile(' ')
+ def htmlize(self, text) :
+ return text
+ pre_markup = self._preformat(text)
+ #print "Preformatted '"+pre_markup+"'"
+ return self._markup( pre_markup )
+ def _markup(self, text) :
+ # markdown() implements the text->html Markdown semantics
+ # the str() builtin ensures a nice, printable string
+ return str(markdown(text))
+ def _preformat(self, text) :
+ #print "Preformatting '"+text+"'"
+ return self._autolink(text)
+ def _autolink(self, text) :
+ "Replace linkexps in text with Markdown syntax versions"
+ # sub passes a match object to link2markup for each match
+ # see http://docs.python.org/lib/match-objects.html
+ return self.linkexp.sub(self.matchlink2markup, text)
+ def matchlink2markup(self, match) :
+ # The name is the first (and only) matched region
+ #print match.groups()
+ name = match.groups()[1]
+ text = match.groups()[0]
+ return self.link2markup(text, name)
+ def linklist(self, text) :
+ linklist = [] # empty list
+ for m in self.linkexp.finditer(text) :
+ linklist.append( self.str2name(m.groups()[1]) )
+ #print linklist
+ return linklist
+ def link2markup(self, text, name) :
+ """
+ usage: string = link2markup(text, target)
+ takes the string name of a wiki page,
+ and returns a string that will be markuped into a link to that page
+ displaying the text text.
+ """
+ # convert to Markdown pretty link syntax
+ if text : # remove trailing |
+ text = text.rstrip('|')
+ else :
+ text = self.name2str(name)
+ name = self.str2name(name)
+ return str('['+text+'](/'+name+')')
+ def name2str(self, name) :
+ """
+ usage: string = name2str(name)
+ takes the string name of a wiki page,
+ and converts it to display format
+ See str2name()
+ """
+ w_spaces = self.uscore.sub(' ', name)
+ return w_spaces
+ def str2name(self, string) :
+ """
+ usage: string = name2str(name)
+ Converts strings to the relevent wiki page name.
+ See str2name()
+ """
+ wo_spaces = self.space.sub('_', string)
+ cap_first = upper(wo_spaces[0]) + wo_spaces[1:]
+ return cap_first
+ def make_add_button(self, next_db_id) :
+ string = '<form action="/edit/%d" method="get">\n' % next_db_id
+ string += ' <input type="submit" value="Add entry">\n'
+ string += '</form><br>\n'
+ return string
+ def make_table(self, fields, records, width_max=WIDTH_MAX,
+ raw_fields=()):
+ """
+ >>> print make_table(['A','B','C'],[{'A':'a','B':'b'},{'A':'d','B':'e','C':'f'}]),
+ <table>
+ <tr><td><b>A</b></td><td><b>B</b></td><td><b>C</b></td></tr>
+ <tr><td>a</td><td>b</td><td></td></tr>
+ <tr><td>d</td><td>e</td><td>f</td></tr>
+ </table>
+ """
+ # open the table
+ string = '<table>\n'
+ # add the header
+ string += '<tr>'
+ for field in fields :
+ string += '<td><b>%s<b></td>' % self.htmlize(field)
+ string += '</tr>\n'
+ # add the records
+ for record in records :
+ string += '<tr>'
+ for field in fields :
+ rstring = record[field]
+ # truncate if the width is regulated...
+ if field in width_max :
+ w = width_max[field]
+ # ... and the full string is too long
+ if len(rstring) > w :
+ rstring = "%s..." % rstring[:w]
+ # if the field isn't raw, protect special chars
+ if not field in raw_fields :
+ rstring = self.htmlize(rstring)
+ string += '<td>%s</td>' % rstring
+ string += '</tr>\n'
+ # close the table
+ string += '</table>\n'
+ return string
+ def record_form_entries(self, index, fields, lfields, record, MSDSs=None, entry_width=ENTRY_WIDTH):
+ """
+ >>> print record_form_entries(4,['A','B','MSDS'],{'A':'AA','B':'BB','MSDS'},{'A':'a','B':'b'}),
+ <table>
+ <tr><td><label for="A">AA</label></td><td>:</td>
+ <td><input type="text" size="50" id="A" name="A" value="a"></td></tr>
+ <tr><td><label for="B">BB</label></td><td>:</td>
+ <td><input type="text" size="50" id="B" name="B" value="b"></td></tr>
+ <tr><td><label for="MSDS">MSDS</label></td><td>:</td>
+ <td><input type="file" size="50" id="MSDS" name="MSDS" value=""></td></tr>
+ </table>
+ """
+ # open the table
+ string = '<table>\n'
+ # add the record fields
+ for field in fields :
+ # get the record string
+ rstring = self.htmlize(record[field])
+ fstring = self.htmlize(field)
+ lfstring = self.htmlize(lfields[field])
+ string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' % (fstring, lfstring)
+ string += ' <td><input type="text" size="%d" id="%s" name="%s" value="%s"></td></tr>\n' \
+ % (entry_width, fstring, fstring, rstring)
+ if MSDSs != None :
+ ## add an MSDS radio, share menu, and file upload fields
+ # radio
+ lfstring = fstring = 'MSDS source'
+ string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' \
+ % (fstring, lfstring)
+ string += ' <td><input type="radio" id="%s" name="%s" value="upload" checked>Upload\n' \
+ % (fstring, fstring)
+ string += ' <input type="radio" id="%s" name="%s" value="share">Share</td></tr>\n' \
+ % (fstring, fstring)
+
+ # file upload
+ fstring = 'MSDS'
+ lfstring = 'Upload MSDS'
+ string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' \
+ % (fstring, lfstring)
+ string += ' <td><input type="file" size="%d" id="%s" name="%s" accept="text/plain,text/html,application/pdf" value=""></td></tr>\n' \
+ % (entry_width, fstring, fstring)
+
+ # file share menu
+ fstring = 'MSDS share'
+ lfstring = 'Use an already uploaded MSDS'
+ string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' \
+ % (fstring, lfstring)
+ string += ' <td><select size="1" id="%s" name="%s">\n' \
+ % (fstring, fstring)
+ for MSDS in MSDSs :
+ lfstring = self.htmlize(MSDS['Name'])
+ id = int(MSDS['ID'])
+ string += ' <option value="%d">%d: %s</option>\n' \
+ % (id, id, lfstring)
+ string += ' </select></td></tr>\n'
+ # close the table
+ string += '</table><br>\n'
+ return string
+
+class templt (object) :
+ "Handle some template wrapping"
+ def __init__(self) :
+ self.dir = 'templates/'
+ if not os.path.exists(self.dir): os.mkdir(self.dir)
+ ## Write simple template html pages if they don't already exist.
+ # see ( http://webpy.org/templetor ) for the syntax.
+ html=('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"\n'
+ ' "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n'
+ '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n')
+ if not os.path.exists(self.dir+'view.html') :
+ view=('$def with (name, next_db_id, fields, records)\n'
+ +html+
+ ' <head>\n'
+ ' <title>\n'
+ ' $:name2str(name) \n'
+ ' </title>\n'
+ ' </head>\n'
+ ' <body>\n'
+ ' <h1>\n'
+ ' $:name2str(name)\n'
+ ' </h1>\n'
+ ' See \n'
+ ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm">the rules</a>\n'
+ ' for more information.\n'
+ ' See the <a href="/docs/">docs</a> page to generate required documents.<br>\n'
+ ' <br>\n'
+ ' $:make_add_button(next_db_id)\n'
+ ' $:make_table(fields, records)\n'
+ ' $:make_add_button(next_db_id)\n'
+ ' </body>\n'
+ '</html>\n')
+ file(self.dir+'view.html', 'w').write(view)
+ if not os.path.exists(self.dir+'edit.html') :
+ # note:
+ # the form encoding type 'enctype="multipart/form-data">\n'
+ # is only required because of the MSDS file-upload field.
+ edit=('$def with (index, fields, lfields, record, MSDSs)\n'
+ +html+
+ ' <head>\n'
+ ' <title>\n'
+ ' $:htmlize(record["Name"]) \n'
+ ' </title>\n'
+ ' </head>\n'
+ ' <body>\n'
+ ' <a style="cursor:pointer;" onclick="history.back()"><h1>\n'
+ ' Editing: $:htmlize(record["Name"]) \n'
+ ' </h1></a>\n'
+ ' <form action="" method="post" enctype="multipart/form-data">\n'
+ ' $:record_form(index, fields, lfields, record, MSDSs)<br />\n'
+ ' <input type="submit" value="Submit" />\n'
+ ' </form>\n'
+ ' </body>\n'
+ '</html>\n')
+ file(self.dir+'edit.html', 'w').write(edit)
+ if not os.path.exists(self.dir+'error.html') :
+ # like view, but no edit ability
+ # since the content is generated,
+ # or backlink list, since there would be so many.
+ stat=('$def with (id)\n'
+ +html+
+ ' <head>\n'
+ ' </head>\n'
+ ' <body>\n'
+ ' <h1>Error</h1>\n'
+ ' There is currently no MSDS file for \n'
+ ' $:htmlize(id)\n'
+ ' </body>\n'
+ '</html>\n')
+ file(self.dir+'error.html', 'w').write(stat)
+ if not os.path.exists(self.dir+'docs.html') :
+ docs=(html +
+ '<a href="/docs/inventory.pdf">Inventory</a>\n'
+ ' in accordance with the \n'
+ ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">\n'
+ ' Chemical Hygiene Plan Section E-7</a>.<br>\n'
+ '<a href="/docs/door_warning.pdf">Door warning</a>\n'
+ ' in accordance with the Chemical Hygiene Plan Sections\n'
+ ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">E-7</a>\n'
+ ' and \n'
+ ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe10">E-10</a>\n'
+ '.<br>\n'
+ ' </body>\n'
+ '</html>\n')
+ file(self.dir+'docs.html', 'w').write(docs)
+
+
+class chem_web_daemon (daemon.Daemon) :
+ gid = None
+ uid = None
+ pidfile = PID_FILE
+ logfile = LOG_FILE
+ loglevel = 'INFO'
+ def run(self) :
+ if self.pkey_file == None or self.cert_file == None :
+ logging.info("http://%s:%d/" % web.validip(self.options.ip))
+ else :
+ logging.info("https://%s:%d/" % web.validip(self.options.ip))
+
+ ## web.py should give detailed error messages, not just `internal error'
+ web.internalerror = web.debugerror
+
+ ## How we'd start webpy server if we didn't need command line args or SSL
+ # Pass web my URL regexps, and the globals from this script
+ # You can also pass it web.profiler to help optimize for speed
+ #web.run(urls, globals())
+ ## How we have to start it now
+ webpy_func = web.webpyfunc(urls, globals(), False)
+ wsgi_func = web.wsgifunc(webpy_func)
+ web.httpserver.runsecure(wsgi_func,
+ web.validip(self.options.ip),
+ ssl_private_key_filename=self.pkey_file,
+ ssl_certificate_filename=self.cert_file)
+
+ def read_basic_config(self) :
+ pass
+ def parse_options(self):
+ from optparse import OptionParser
+
+ usage_string = ('%prog [options]\n'
+ '\n'
+ '2008, W. Trevor King.\n')
+ version_string = '%%prog %s' % __version__
+ parser = OptionParser(usage=usage_string, version=version_string)
+
+ # Daemon options
+ parser.add_option('--start', dest='action',
+ action='store_const', const='start', default='start',
+ help='Start the daemon (the default action)')
+ parser.add_option('--stop', dest='action',
+ action='store_const', const='stop', default='start',
+ help='Stop the daemon')
+ parser.add_option('-n', '--nodaemon', dest='daemonize',
+ action='store_false', default=True,
+ help='Run in the foreground')
+
+ # Server options
+ parser.add_option('-a', '--ip-address', dest="ip",
+ help="IP address (default '%default')",
+ type='string', metavar="IP", default='0.0.0.0:8080')
+ parser.add_option('-s', '--secure', dest="secure",
+ help="Run in secure (HTTPS) mode.",
+ type='string', metavar="PKEY_FILE:CERT_FILE")
+ parser.add_option('-v', '--verbose', dest="verbose", action="store_true",
+ help="Print lots of debugging information",
+ default=False)
+
+ self.options, self.args = parser.parse_args()
+ parser.destroy()
+
+ if self.options.verbose :
+ self.loglevel = 'DEBUG'
+ # get options for httpserver
+ if self.options.secure != None :
+ split = self.options.secure.split(':')
+ assert len(split) == 2, "Invalid secure argument '%s'"
+ self.pkey_file = split[0]
+ self.cert_file = split[1]
+ else :
+ self.pkey_file = None
+ self.cert_file = None
+
+### the following instances and definitions must have
+## global scope. because they are called from inside web
+# create the database
+db = database(filename=DB_FILE)
+MSDSman = MSDS_manager(db)
+dgen = docgen(db)
+
+## set up the templates
+tmplt = templt()
+# Tell templator where to look for templates
+# to provide a framework for the generated webpages
+render = web.template.render(tmplt.dir)
+
+## Define markup functions
+mkup = markup()
+htmlize = lambda t : mkup.htmlize(t)
+name2str = lambda n : mkup.name2str(n)
+mktable = lambda h,r : mkup.make_table(h,r,WIDTH_MAX,raw_fields=('ID','Name'))
+mkadd = lambda ni : mkup.make_add_button(ni)
+record_form = lambda i,f,l,r,M : mkup.record_form_entries(i,f,l,r,M)
+## Give templates access to the htmlize, make_table, and name2str functions
+# ( see http://webpy.org/templetor and our view.html )
+web.template.Template.globals['htmlize'] = htmlize
+web.template.Template.globals['make_table'] = mktable
+web.template.Template.globals['make_add_button'] = mkadd
+web.template.Template.globals['name2str'] = name2str
+web.template.Template.globals['record_form'] = record_form
+
+
+## If this script is run from the command line,
+# use the tools we've just defined to host the chemical inventory
+if __name__=="__main__":
+ chem_web_daemon().main()
--- /dev/null
+#!/usr/bin/python
+#
+# Web application for posting additions/modifications to the chem-inventory
+# derived from simplewiki and wiki_py, by Adam Bachman
+# ( http://bachman.infogami.com/ )
+# Authentication following Snakelets
+# http://snakelets.sourceforge.net/manual/authorization.html
+
+# Remember to delete the templates directory after making alterations,
+# because this program only generates templates if they are missing.
+
+
+## Bring in some useful goodies:
+# web, because we're running a web server, and generating html;
+# time, for timestamping backups;
+# os, for creating the template directories; and
+# re, for replacing internal node links with proper markup
+import web, os, re
+# md5, for hashing passwords
+#import md5
+
+# database must support the following methods :
+# field_list() : return an ordered list of available fields (fields unique)
+# long_field() : return an dict of long field names (keyed by field names)
+# record(id) : return a record dict (keyed by field names)
+# records() : return an ordered list of available records.
+# backup() : save a copy of the current database somehow.
+# set_record(i, newvals) : set a record by overwriting any preexisting data
+# : with data from the field-name-keyed dict NEWVALS
+import text_db
+database = text_db.text_db
+from chem_db import valid_CASno, MSDS_manager, docgen
+DB_FILE = 'inventory.db'
+import sys, daemon, logging
+PID_FILE = './chem_web_secure.pid'
+LOG_FILE = './chem_web_secure.log'
+
+## Also import a markup language, and associated regexp functions
+from markdown import markdown
+## for converting the first character in record names, and stripping |
+from string import upper, rstrip
+
+## for SSL
+SSL = True
+if SSL :
+ from mk_simple_certs import get_cert_filenames
+ server_name = 'chem_web'
+
+__version__ = "0.2"
+
+## For web.py, when a URL matches '/view', handle with class 'view'
+# when it matches '/edit', handle with class 'edit',
+# and when it matches '/login', handle with class 'login',
+# Note, there are no .html extension.
+#
+# link to edit pages using db_id
+# link to MSDSs with ID
+urls = ('/(view)?', 'view',
+ '/edit/([0-9]+)', 'edit',
+ '/MSDS/([0-9]+)', 'MSDS',
+ '/docs/([a-z._]*)', 'docs',
+ '/login', 'login')
+
+# limit the display width of selected fields
+WIDTH_MAX = {'CAS#':12}
+
+# set the display width of entry fields
+ENTRY_WIDTH = 50
+
+class view (object) :
+ "Print the database"
+ # when the server recieves the command "GET '/view', call view.get('view')
+ def GET(self, name):
+ """
+ Render the view.html template using name and the previous text.
+ If the file doesn't exist, use default text '%s doesn't exist'.
+ """
+ if name == None :
+ name = 'view'
+ db._refresh()
+ recs = []
+ for record in db.records() :
+ id = int(record['ID'])
+ record['ID'] = '<a href="/edit/%d">%s</a>' \
+ % (id, record['ID'])
+ if MSDSman.has_MSDS(id) : # link the id to the MSDS
+ record['Name'] = '<a href="/MSDS/%d">%s</a>' \
+ % (record['db_id'], record['Name'])
+ recs.append(record)
+ print render.view('Chemical inventory',
+ db.len_records(), db.field_list(), recs)
+
+class edit (object) :
+ "Provide methods for handling requests related to edit pages"
+ def GET(self, name):
+ """
+ Render the edit.html template using the specified record number.
+ """
+ db._refresh()
+ db_id = int(name)
+ if db_id >= db.len_records() :
+ logging.info("new record")
+ assert db_id == db.len_records(), \
+ 'invalid id: %d (max %d)' % (db_id, db.len_records())
+ record = db.new_record()
+ record['ID'] = str(record['db_id'])
+ db.set_record(db_id, record)
+ if MSDSman.has_MSDS(db_id) :
+ MSDSs = None
+ else :
+ # only ask for an MSDS if we still need one
+ MSDSs = MSDSman.get_all(simlinks=False)
+ print render.edit(int(name), db.field_list(),
+ db.long_fields(), db.record(int(name)), MSDSs)
+ def POST(self, name):
+ """
+ Read the form input and update the database accordingly.
+ Then redirect to /view.
+ """
+ db._refresh()
+ record = db.record(int(name))
+ ## Generate a new record from the form input
+ # 'MSDS={}' to storify MSDS as a FieldStorage (dict-like) instance,
+ # otherwise it's saved as a string, and we loose info about it.
+ # e.g. newvals['MSDS'].type = 'text/html'
+ # The contents of the file are saved to newvals['MSDS'].value
+ newvals = web.input(MSDS={})
+ update = False
+ for field in db.field_list() :
+ if newvals[field] != record[field] :
+ update=True
+ record[field] = newvals[field]
+ if 'MSDS source' in newvals :
+ # Handle any MSDS file actions
+ if newvals['MSDS source'] == 'upload' :
+ if len(newvals['MSDS'].filename) > 0 and len(newvals['MSDS'].value) > 0:
+ # only save if there is a file there to save ;)
+ #print >> stderr, web.ctx.env['CONTENT_TYPE']
+ #print >> stderr, web.ctx.keys()
+ #print >> stderr, dir(newvals['MSDS'])
+ #print >> stderr, type(newvals['MSDS'])
+ #print >> stderr, newvals['MSDS'].filename
+ #print >> stderr, newvals['MSDS'].type
+ MSDSman.save(int(name), newvals['MSDS'].value,
+ newvals['MSDS'].type)
+ else :
+ logging.info('linking MSDS %d to %d' \
+ % (int(record['ID']),
+ int(newvals['MSDS share'])) )
+ MSDSman.link(int(record['ID']),
+ int(newvals['MSDS share']))
+ if update :
+ db.set_record(int(name), record, backup=True)
+ # redirect to view all
+ web.seeother('/view')
+
+class MSDS (object) :
+ "Serve MSDS files by ID"
+ def GET(self, name):
+ "Serve MSDS files by ID"
+ id = int(name)
+ if MSDSman.has_MSDS(id) :
+ mime = MSDSman.get_MSDS_MIME(id)
+ web.header("Content-Type", mime)
+ print file(MSDSman.get_MSDS_path(id), 'rb').read() ,
+ else :
+ print render.error(id)
+
+class docs (object) :
+ "Generate and serve assorted official documents"
+ def GET(self, name):
+ """
+ List the available documents.
+ """
+ db._refresh()
+ if name == '' :
+ print render.docs()
+ if name == 'inventory.pdf' :
+ path = dgen.inventory(namewidth=40)
+ print file(path, 'rb').read() ,
+ if name == 'door_warning.pdf' :
+ path = dgen.door_warning()
+ print file(path, 'rb').read() ,
+
+
+class login (object) :
+ "Print an alphabetized index of all existing pages"
+ def GET(self):
+ print "Not yet implemented"
+ #print render.stat('login')
+
+class markup (object) :
+ "Convert text to html, using Markdown with a bit of preformatting"
+ def __init__(self) :
+ # [[optional display text|name with or_without_spaces]]
+ linkregexp = ('\[\['
+ +'([^\|\[\]]*\|)?'
+ +'([a-zA-Z0-9-_ ]*)'
+ +'\]\]')
+ #print linkregexp
+ self.linkexp = re.compile(linkregexp)
+ self.linebreak = ' \n'
+ self.uscore = re.compile('_')
+ self.space = re.compile(' ')
+ def htmlize(self, text) :
+ return text
+ pre_markup = self._preformat(text)
+ #print "Preformatted '"+pre_markup+"'"
+ return self._markup( pre_markup )
+ def _markup(self, text) :
+ # markdown() implements the text->html Markdown semantics
+ # the str() builtin ensures a nice, printable string
+ return str(markdown(text))
+ def _preformat(self, text) :
+ #print "Preformatting '"+text+"'"
+ return self._autolink(text)
+ def _autolink(self, text) :
+ "Replace linkexps in text with Markdown syntax versions"
+ # sub passes a match object to link2markup for each match
+ # see http://docs.python.org/lib/match-objects.html
+ return self.linkexp.sub(self.matchlink2markup, text)
+ def matchlink2markup(self, match) :
+ # The name is the first (and only) matched region
+ #print match.groups()
+ name = match.groups()[1]
+ text = match.groups()[0]
+ return self.link2markup(text, name)
+ def linklist(self, text) :
+ linklist = [] # empty list
+ for m in self.linkexp.finditer(text) :
+ linklist.append( self.str2name(m.groups()[1]) )
+ #print linklist
+ return linklist
+ def link2markup(self, text, name) :
+ """
+ usage: string = link2markup(text, target)
+ takes the string name of a wiki page,
+ and returns a string that will be markuped into a link to that page
+ displaying the text text.
+ """
+ # convert to Markdown pretty link syntax
+ if text : # remove trailing |
+ text = text.rstrip('|')
+ else :
+ text = self.name2str(name)
+ name = self.str2name(name)
+ return str('['+text+'](/'+name+')')
+ def name2str(self, name) :
+ """
+ usage: string = name2str(name)
+ takes the string name of a wiki page,
+ and converts it to display format
+ See str2name()
+ """
+ w_spaces = self.uscore.sub(' ', name)
+ return w_spaces
+ def str2name(self, string) :
+ """
+ usage: string = name2str(name)
+ Converts strings to the relevent wiki page name.
+ See str2name()
+ """
+ wo_spaces = self.space.sub('_', string)
+ cap_first = upper(wo_spaces[0]) + wo_spaces[1:]
+ return cap_first
+ def make_add_button(self, next_db_id) :
+ string = '<form action="/edit/%d" method="get">\n' % next_db_id
+ string += ' <input type="submit" value="Add entry">\n'
+ string += '</form><br>\n'
+ return string
+ def make_table(self, fields, records, width_max=WIDTH_MAX,
+ raw_fields=()):
+ """
+ >>> print make_table(['A','B','C'],[{'A':'a','B':'b'},{'A':'d','B':'e','C':'f'}]),
+ <table>
+ <tr><td><b>A</b></td><td><b>B</b></td><td><b>C</b></td></tr>
+ <tr><td>a</td><td>b</td><td></td></tr>
+ <tr><td>d</td><td>e</td><td>f</td></tr>
+ </table>
+ """
+ # open the table
+ string = '<table>\n'
+ # add the header
+ string += '<tr>'
+ for field in fields :
+ string += '<td><b>%s<b></td>' % self.htmlize(field)
+ string += '</tr>\n'
+ # add the records
+ for record in records :
+ string += '<tr>'
+ for field in fields :
+ rstring = record[field]
+ # truncate if the width is regulated...
+ if field in width_max :
+ w = width_max[field]
+ # ... and the full string is too long
+ if len(rstring) > w :
+ rstring = "%s..." % rstring[:w]
+ # if the field isn't raw, protect special chars
+ if not field in raw_fields :
+ rstring = self.htmlize(rstring)
+ string += '<td>%s</td>' % rstring
+ string += '</tr>\n'
+ # close the table
+ string += '</table>\n'
+ return string
+ def record_form_entries(self, index, fields, lfields, record, MSDSs=None, entry_width=ENTRY_WIDTH):
+ """
+ >>> print record_form_entries(4,['A','B','MSDS'],{'A':'AA','B':'BB','MSDS'},{'A':'a','B':'b'}),
+ <table>
+ <tr><td><label for="A">AA</label></td><td>:</td>
+ <td><input type="text" size="50" id="A" name="A" value="a"></td></tr>
+ <tr><td><label for="B">BB</label></td><td>:</td>
+ <td><input type="text" size="50" id="B" name="B" value="b"></td></tr>
+ <tr><td><label for="MSDS">MSDS</label></td><td>:</td>
+ <td><input type="file" size="50" id="MSDS" name="MSDS" value=""></td></tr>
+ </table>
+ """
+ # open the table
+ string = '<table>\n'
+ # add the record fields
+ for field in fields :
+ # get the record string
+ rstring = self.htmlize(record[field])
+ fstring = self.htmlize(field)
+ lfstring = self.htmlize(lfields[field])
+ string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' % (fstring, lfstring)
+ string += ' <td><input type="text" size="%d" id="%s" name="%s" value="%s"></td></tr>\n' \
+ % (entry_width, fstring, fstring, rstring)
+ if MSDSs != None :
+ ## add an MSDS radio, share menu, and file upload fields
+ # radio
+ lfstring = fstring = 'MSDS source'
+ string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' \
+ % (fstring, lfstring)
+ string += ' <td><input type="radio" id="%s" name="%s" value="upload" checked>Upload\n' \
+ % (fstring, fstring)
+ string += ' <input type="radio" id="%s" name="%s" value="share">Share</td></tr>\n' \
+ % (fstring, fstring)
+
+ # file upload
+ fstring = 'MSDS'
+ lfstring = 'Upload MSDS'
+ string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' \
+ % (fstring, lfstring)
+ string += ' <td><input type="file" size="%d" id="%s" name="%s" accept="text/plain,text/html,application/pdf" value=""></td></tr>\n' \
+ % (entry_width, fstring, fstring)
+
+ # file share menu
+ fstring = 'MSDS share'
+ lfstring = 'Use an already uploaded MSDS'
+ string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' \
+ % (fstring, lfstring)
+ string += ' <td><select size="1" id="%s" name="%s">\n' \
+ % (fstring, fstring)
+ for MSDS in MSDSs :
+ lfstring = self.htmlize(MSDS['Name'])
+ id = int(MSDS['ID'])
+ string += ' <option value="%d">%d: %s</option>\n' \
+ % (id, id, lfstring)
+ string += ' </select></td></tr>\n'
+ # close the table
+ string += '</table><br>\n'
+ return string
+
+class templt (object) :
+ "Handle some template wrapping"
+ def __init__(self) :
+ self.dir = 'templates/'
+ if not os.path.exists(self.dir): os.mkdir(self.dir)
+ ## Write simple template html pages if they don't already exist.
+ # see ( http://webpy.org/templetor ) for the syntax.
+ html=('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"\n'
+ ' "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n'
+ '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n')
+ if not os.path.exists(self.dir+'view.html') :
+ view=('$def with (name, next_db_id, fields, records)\n'
+ +html+
+ ' <head>\n'
+ ' <title>\n'
+ ' $:name2str(name) \n'
+ ' </title>\n'
+ ' </head>\n'
+ ' <body>\n'
+ ' <h1>\n'
+ ' $:name2str(name)\n'
+ ' </h1>\n'
+ ' See \n'
+ ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm">the rules</a>\n'
+ ' for more information.\n'
+ ' See the <a href="/docs/">docs</a> page to generate required documents.<br>\n'
+ ' <br>\n'
+ ' $:make_add_button(next_db_id)\n'
+ ' $:make_table(fields, records)\n'
+ ' $:make_add_button(next_db_id)\n'
+ ' </body>\n'
+ '</html>\n')
+ file(self.dir+'view.html', 'w').write(view)
+ if not os.path.exists(self.dir+'edit.html') :
+ # note:
+ # the form encoding type 'enctype="multipart/form-data">\n'
+ # is only required because of the MSDS file-upload field.
+ edit=('$def with (index, fields, lfields, record, MSDSs)\n'
+ +html+
+ ' <head>\n'
+ ' <title>\n'
+ ' $:htmlize(record["Name"]) \n'
+ ' </title>\n'
+ ' </head>\n'
+ ' <body>\n'
+ ' <a style="cursor:pointer;" onclick="history.back()"><h1>\n'
+ ' Editing: $:htmlize(record["Name"]) \n'
+ ' </h1></a>\n'
+ ' <form action="" method="post" enctype="multipart/form-data">\n'
+ ' $:record_form(index, fields, lfields, record, MSDSs)<br />\n'
+ ' <input type="submit" value="Submit" />\n'
+ ' </form>\n'
+ ' </body>\n'
+ '</html>\n')
+ file(self.dir+'edit.html', 'w').write(edit)
+ if not os.path.exists(self.dir+'error.html') :
+ # like view, but no edit ability
+ # since the content is generated,
+ # or backlink list, since there would be so many.
+ stat=('$def with (id)\n'
+ +html+
+ ' <head>\n'
+ ' </head>\n'
+ ' <body>\n'
+ ' <h1>Error</h1>\n'
+ ' There is currently no MSDS file for \n'
+ ' $:htmlize(id)\n'
+ ' </body>\n'
+ '</html>\n')
+ file(self.dir+'error.html', 'w').write(stat)
+ if not os.path.exists(self.dir+'docs.html') :
+ docs=(html +
+ '<a href="/docs/inventory.pdf">Inventory</a>\n'
+ ' in accordance with the \n'
+ ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">\n'
+ ' Chemical Hygiene Plan Section E-7</a>.<br>\n'
+ '<a href="/docs/door_warning.pdf">Door warning</a>\n'
+ ' in accordance with the Chemical Hygiene Plan Sections\n'
+ ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">E-7</a>\n'
+ ' and \n'
+ ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe10">E-10</a>\n'
+ '.<br>\n'
+ ' </body>\n'
+ '</html>\n')
+ file(self.dir+'docs.html', 'w').write(docs)
+
+
+class chem_web_daemon (daemon.Daemon) :
+ gid = None
+ uid = None
+ pidfile = PID_FILE
+ logfile = LOG_FILE
+ loglevel = 'INFO'
+ def run(self) :
+ if self.pkey_file == None or self.cert_file == None :
+ logging.info("http://%s:%d/" % web.validip(self.options.ip))
+ else :
+ logging.info("https://%s:%d/" % web.validip(self.options.ip))
+
+ ## web.py should give detailed error messages, not just `internal error'
+ web.internalerror = web.debugerror
+
+ ## How we'd start webpy server if we didn't need command line args or SSL
+ # Pass web my URL regexps, and the globals from this script
+ # You can also pass it web.profiler to help optimize for speed
+ #web.run(urls, globals())
+ ## How we have to start it now
+ webpy_func = web.webpyfunc(urls, globals(), False)
+ wsgi_func = web.wsgifunc(webpy_func)
+ web.httpserver.runsecure(wsgi_func,
+ web.validip(self.options.ip),
+ ssl_private_key_filename=self.pkey_file,
+ ssl_certificate_filename=self.cert_file)
+
+ def read_basic_config(self) :
+ pass
+ def parse_options(self):
+ from optparse import OptionParser
+
+ usage_string = ('%prog [options]\n'
+ '\n'
+ '2008, W. Trevor King.\n')
+ version_string = '%%prog %s' % __version__
+ parser = OptionParser(usage=usage_string, version=version_string)
+
+ # Daemon options
+ parser.add_option('--start', dest='action',
+ action='store_const', const='start', default='start',
+ help='Start the daemon (the default action)')
+ parser.add_option('--stop', dest='action',
+ action='store_const', const='stop', default='start',
+ help='Stop the daemon')
+ parser.add_option('-n', '--nodaemon', dest='daemonize',
+ action='store_false', default=True,
+ help='Run in the foreground')
+
+ # Server options
+ parser.add_option('-a', '--ip-address', dest="ip",
+ help="IP address (default '%default')",
+ type='string', metavar="IP", default='0.0.0.0:8080')
+ parser.add_option('-s', '--secure', dest="secure",
+ help="Run in secure (HTTPS) mode.",
+ type='string', metavar="PKEY_FILE:CERT_FILE")
+ parser.add_option('-v', '--verbose', dest="verbose", action="store_true",
+ help="Print lots of debugging information",
+ default=False)
+
+ self.options, self.args = parser.parse_args()
+ parser.destroy()
+
+ if self.options.verbose :
+ self.loglevel = 'DEBUG'
+ # get options for httpserver
+ if self.options.secure != None :
+ split = self.options.secure.split(':')
+ assert len(split) == 2, "Invalid secure argument '%s'"
+ self.pkey_file = split[0]
+ self.cert_file = split[1]
+ else :
+ self.pkey_file = None
+ self.cert_file = None
+
+### the following instances and definitions must have
+## global scope. because they are called from inside web
+# create the database
+db = database(filename=DB_FILE)
+MSDSman = MSDS_manager(db)
+dgen = docgen(db)
+
+## set up the templates
+tmplt = templt()
+# Tell templator where to look for templates
+# to provide a framework for the generated webpages
+render = web.template.render(tmplt.dir)
+
+## Define markup functions
+mkup = markup()
+htmlize = lambda t : mkup.htmlize(t)
+name2str = lambda n : mkup.name2str(n)
+mktable = lambda h,r : mkup.make_table(h,r,WIDTH_MAX,raw_fields=('ID','Name'))
+mkadd = lambda ni : mkup.make_add_button(ni)
+record_form = lambda i,f,l,r,M : mkup.record_form_entries(i,f,l,r,M)
+## Give templates access to the htmlize, make_table, and name2str functions
+# ( see http://webpy.org/templetor and our view.html )
+web.template.Template.globals['htmlize'] = htmlize
+web.template.Template.globals['make_table'] = mktable
+web.template.Template.globals['make_add_button'] = mkadd
+web.template.Template.globals['name2str'] = name2str
+web.template.Template.globals['record_form'] = record_form
+
+
+## If this script is run from the command line,
+# use the tools we've just defined to host the chemical inventory
+if __name__=="__main__":
+ chem_web_daemon().main()
--- /dev/null
+# Produce html, pdf, and dvi output from latex source
+# W. Trevor King, 2008, version 0.2
+# This file is in the public domain.
+#
+# exposed targets :
+# all : generate each of the outputs (pdf, html, dvi) and call view
+# view : call '$(PDF_VIEWER) main.pdf &' to see the pdf file
+# install : call install_% for pdf, html, dvi, and dist
+# clean : semi-clean and remove the $(GENERATED_FILES) as well
+# semi-clean : remove all intermediate $(TEMP_FILES) used during generation
+#
+# pdf : generate the pdf file (Portable Document Format)
+# install_pdf : install the dvi file (currently no action)
+# html : generate html directory ready for posting
+# install_html : scp the html directory to $(INSTALL_HTML)
+# dvi : generate main.dvi (DeVice Independent file)
+# install_dvi : install the dvi file (currently no action)
+# dist : gen. $(DOC_NAME)-$(VERSION).tar.gz containing $(DIST_FILES)
+# install_dist : install the dist file (currently no action)
+#
+# images : call for generation of all images in $(IMAGES)
+
+DOC_NAME = chem_inventory
+VERSION = 0.2
+
+# I like to keep my images in seperate directories, where any processing
+# they may need woln't clutter up the base directory with temp files.
+# To control these, this Makefile calls the Makefiles in the IMAGE_DIRS,
+# so make sure they exist, and have the following targets:
+# clean : remove all generated files
+# semi-clean : remove all intermediate files
+# for each image in IMAGE, you must have a rule that generates it,
+# see 'image generation rules' below
+IMAGES = mp/NFPA.1
+IMAGE_DIRS = mp
+
+# Non image source files
+SOURCE_FILES = main.tex *.tex
+# And anything else you'd like to distribute
+OTHER_FILES = README Makefile
+
+DIST_FILES = $(SOURCE_FILES) $(OTHER_FILES) $(IMAGE_DIRS)
+
+# Select where to put things when you call the dist and install targets
+DIST_DIR = $(DOC_NAME)-$(VERSION)
+DIST_NAME = $(DOC_NAME)-$(VERSION).tar.gz
+INSTALL_PDF = $(HOME)/rsrch/notes
+INSTALL_DVI =
+INSTALL_HTML = einstein:./public_html/rsrch/papers/$(DOC_NAME)
+# interpreted as latex by latex2html, so escape the '~' to '\~{}'
+TILDE = "%7E"
+CSS_PATH = "/$(TILDE)wking/shared/style_l2h.css"
+
+# Files removed on clean or semi-clean
+TEMP_FILES = *[.]aux *[.]log *[.]out *[.]bak \
+ *.png *.eps
+# Files removed only on clean
+GENERATED_FILES = $(DIST_NAME) \
+ rm -f main.pdf main.dvi html *.pdf
+
+PDF_VIEWER = xpdf # you can also try evince, acroread, 'xpdf -view H', etc.
+
+## image generation rules, to handle the images in $(IMAGES)
+
+mp/NFPA.1 :
+ $(MAKE) -C mp images
+
+
+
+### The remaining rules shouldn't need to be changed
+
+# pdf, dvi, and html depend on images
+images : $(IMAGES)
+
+
+## Big targets
+all : pdf html dvi view
+
+view : pdf
+ $(PDF_VIEWER) main.pdf &
+
+install : install_pdf install_html install_dvi install_dist
+
+clean : semi-clean $(IMAGE_DIRS:%=%_clean)
+ rm -rf $(GENERATED_FILES)
+
+semi-clean : $(IMAGE_DIRS:%=%_semi-clean)
+ rm -f $(TEMP_FILES)
+
+
+## Mid-level indirection targets
+
+# indirection, so we can have short names like 'pdf'
+# without rebuilding when it's not neccessary
+pdf : main.pdf
+dvi : main.dvi
+dist : $(DIST_NAME)
+
+
+## Installation targets
+
+install_pdf :
+install_html : html clean_html_image_dirs
+ scp -r html $(INSTALL_HTML)
+install_dvi :
+install_dist :
+
+
+## The meat of the generation
+
+# generate the pdf output directly with pdflatex
+main.pdf : main.aux $(SOURCE_FILE) images
+ pdflatex main.tex
+
+# generate html files, including the DIST_FILES in a subdir.
+# latex2html requires main.aux to generate figure numbers
+# main.pdf is for the '$(DOC_NAME).pdf' link in the footer
+# (see ~/.latex2html-init)
+html : $(SOURCE_FILES) main.aux main.pdf
+ latex2html -split 1 -white -notransparent -html_version 3.2 \
+ -t $(DOC_NAME) -dir html -mkdir \
+ -noshort_extn -top_navigation -bottom_navigation \
+ -up_url '../' -up_title 'Papers' \
+ -show_section_numbers \
+ -style $(CSS_PATH) \
+ main.tex # generate the html
+ if [ ! -d html/src ]; then mkdir html/src ; fi # mk src if it doesn't exist yet
+ cp -r $(DIST_FILES) html/src/ # move the source files into html/src
+ cp main.pdf html/$(DOC_NAME).pdf # move the pdf in so we can link to it
+
+# generate the .aux file which contains labels for referencing
+# needed to correctly number references in the pdf and html output.
+# (Clash between latex and pdflatex's .aux files? semi-clean fixes)
+# -draftmode makes pdflatex ignore images and not make a pdf (faster)
+main.aux : *.tex images
+ pdflatex -draftmode main.tex
+
+# to stretch our .tex files a bit more, make a dvi as well :p
+# run twice to straighten out and TOC or references issues
+main.dvi : $(SOURCE_FILES) images
+ latex main.tex
+ latex main.tex
+
+# generate a gzipped tar file containing the DIST_FILES
+$(DIST_NAME) : $(DIST_FILES)
+ mkdir $(DIST_DIR)
+ cp $(DIST_FILES) $(DIST_DIR)
+ tar -chozf $(DIST_NAME) $(DIST_DIR)
+ rm -rf $(DIST_DIR)
+
+
+## IMAGE_DIR handling rules
+
+# call clean for any image directory
+%_clean : %
+ $(MAKE) -C $< clean
+
+# call semi-clean for any image directory
+%_semi-clean : %
+ $(MAKE) -C $< semi-clean
+
+# clean and junk that was in the working image directories
+clean_html_image_dirs : $(IMAGE_DIRS:%=%_html_dir_clean)
+
+# call clean for any image directory in the html/src directory
+%_html_dir_clean : %
+ $(MAKE) -C html/src/$< clean
--- /dev/null
+The bulk of the content is generated dynamically from the database.
+This directory just contains the latex and infrastructure to make nice
+documents from the data.
+
+The `mp' directory contains metapost source and assorted
+infrastructure for building and previewing the graphics.
+
--- /dev/null
+\begin{tabular}{l}
+ Professor Guoliang Yang\\
+ 215-895-6669 \\
+ Disque Hall 926 \\
+\end{tabular}
--- /dev/null
+% \documentclass[letterpaper]{article} % line moved to main.
+% Paper for posting on the lab door
+% as per
+
+% setup the margins,
+\topmargin -0.5in
+\headheight 0.0in
+\headsep 0.0in
+\textheight 10in
+\oddsidemargin -0.5in
+\textwidth 7.5in
+
+%% Graphics packages
+
+\usepackage{graphicx} % to include images
+% if pdftex doesn't recognize the type, it's mps
+% DeclareGraphicsRule{ext}{type}{sizefile}{command}
+\DeclareGraphicsRule{*}{mps}{*}{}
+% latex and latex2html can handle Metapost's eps output without modification
+
+% define a vertical strut for table spacing
+\newcommand{\Tstrut}{\rule{0pt}{2.6ex}}
+\newcommand{\headfont}{\Large}
+\newcommand{\contfont}{\large}
+\newcommand{\vspacer}{\vskip 12pt}
+
+%-----------------------end preamble-------------------------
+
+\begin{document}
+\thispagestyle{empty} % suppress page numbering
+\sffamily % switch to sans-serif
+
+
+\begin{center}
+
+\includegraphics[width=3in]{mp/NFPA.1}
+\vspacer
+
+\contfont
+\input{door_data}
+
+\vfill
+
+{\headfont Contact}
+\vskip 10pt
+\contfont
+\input{contact}
+\vspacer
+
+Generated \today\ by chem\_db.py
+
+\end{center}
+
+\end{document}
--- /dev/null
+% \documentclass[letterpaper]{article} % line moved to main.
+% Paper for posting on the lab door
+% as per
+
+% setup the margins,
+\topmargin -0.5in
+\headheight 0.0in
+\headsep 0.0in
+\textheight 9.5in % leave a bit of extra room for page numbers
+\oddsidemargin -0.5in
+\textwidth 7.5in
+
+%% Graphics packages
+
+\usepackage{graphicx} % to include images
+% if pdftex doesn't recognize the type, it's mps
+% DeclareGraphicsRule{ext}{type}{sizefile}{command}
+\DeclareGraphicsRule{*}{mps}{*}{}
+% latex and latex2html can handle Metapost's eps output without modification
+
+% N of M style page numbering
+\usepackage{fancyhdr}
+\pagestyle{fancy}
+\usepackage{lastpage}
+\cfoot{\thepage\ of \pageref{LastPage}}
+% Turn off the silly head and foot rules
+\renewcommand{\headrule}{\relax}
+\renewcommand{\footrule}{\relax}
+
+% Break the table across several pages
+% requires 2 latex runs.
+\usepackage{longtable}
+
+% define a vertical strut for table spacing
+\newcommand{\Tstrut}{\rule{0pt}{2.6ex}}
+\newcommand{\headfont}{\Large}
+\newcommand{\contfont}{\large}
+\newcommand{\vspacer}{\vskip 12pt}
+
+%-----------------------end preamble-------------------------
+
+\begin{document}
+
+\begin{center}
+{\headfont Inventory}\\
+\contfont
+Generated \today\ by chem\_db.py\\
+\vskip 10pt
+\end{center}
+
+\footnotesize
+\input{inventory_data}
+
+\end{document}
--- /dev/null
+MPS = NFPA
+
+TEMP_FILES = $(MPS:%=%.mp[a-z]) $(MPS:%=%.log) mp* texnum.mpx
+GENERATED_FILES = $(MPS:%=%.[0-9]*) sample.log sample.aux sample.pdf
+
+#PDFVIEWER = gv
+PDFVIEWER = xpdf
+#PDFVIEWER = evince
+
+images : $(MPS:%=%.1)
+
+all : view
+
+view : sample.pdf
+ $(PDFVIEWER) sample.pdf &
+
+clean : semi-clean
+ rm -f $(GENERATED_FILES)
+
+semi-clean :
+ rm -f $(TEMP_FILES)
+
+# generate a pdf containing all mp images for previewing-troubleshooting
+# depend on the first image from each mp file
+sample.pdf : sample.tex images
+ pdflatex sample.tex
+
+# if we call for the first image from an mp file, make them all
+%.1 : %.mp %_c.mp
+ mpost "\$(^:%=input %;)"
--- /dev/null
+
+%% NFPA warning triangle
+boolean labels;
+labels := false; % turn on debugging labels
+
+% sizing
+numeric u;
+
+u := 2cm; % unit, for overall scaling
+
+% line thicknesses
+numeric cyl_thickness, wire_thickness;
+border := 1pt; % thickness of the box outlines
+
+% colors
+color hcolor, fcolor, rcolor, ocolor, bcolor;
+hcolor := (0,0,1); % health
+fcolor := (1,0,0); % fire
+rcolor := (1,1,0); % reactivity
+ocolor := white; % other
+bcolor := black; % border
+
+% return a diamond path
+vardef diamond(expr radius) =
+ save P;
+ path P;
+ P := (-radius,0) -- (0,radius) -- (radius,0) -- (0,-radius) -- cycle;
+ P
+enddef;
+
+def NFPA(expr center, radius, h, f, r, o) =
+ save p, hr, qr;
+ pair p;
+ numeric hr, hr;
+ hr := radius/2;
+ qr := radius/4;
+ % draw the background colors
+ fill diamond(hr) shifted (center-(hr,0)) withcolor hcolor;
+ fill diamond(hr) shifted (center+(0,hr)) withcolor fcolor;
+ fill diamond(hr) shifted (center+(hr,0)) withcolor rcolor;
+ fill diamond(hr) shifted (center-(0,hr)) withcolor ocolor;
+ % draw the borders
+ draw diamond(radius) shifted center withpen pencircle scaled border
+ withcolor bcolor;
+ p := (hr,hr);
+ draw p -- -p shifted center withpen pencircle scaled border
+ withcolor bcolor;
+ p := (hr,-hr);
+ draw p -- -p shifted center withpen pencircle scaled border
+ withcolor bcolor;
+ % add the text
+ label(h, center-(hr,0));
+ label(f, center+(0,hr));
+ label(r, center+(hr,0));
+ % btex 4 etex,
+ % btex ox etex;
+ % btex \sout{W} etex;
+ label(o, center-(0,hr));
+enddef;
+
+beginfig(1)
+ NFPA(origin, u,
+ h,
+ f,
+ r,
+ o);
+
+endfig;
+
+end
--- /dev/null
+NFPA.mp and NFPA_c.mp are for generating an NFPA warning diamond. The
+diamond code and figure definitions are in NFPA_c.mp (c for code :p).
+NFPA.mp extracts the TeX for creating the symbols for the corners.
+Because this infomation will change with time (depending on the
+chemicals in the lab), you have to recreate NFPA.mp to get an image
+with the current values. Calling
+ $ gen_NFPA.sh <H> <F> <R> <ox?> <w?> > NFPA.mp
+will generate the proper NFPA.mp file. As an explicit example,
+try the complete pdf generation and viewing commands
+ $ gen_NFPA.sh 4 3 3 y y > NFPA.mp
+ $ make view
+NFPA_c.mp contains the common code for drawing warning diamonds.
+sample.tex is just a simple latex script for previewing diamonds.
--- /dev/null
+#!/bin/bash
+#
+# Generate NFPA.mp for given hazard levels
+
+HEALTH=$1
+FIRE=$2
+REACT=$3
+OX=$4
+W=$5
+
+REDIRECT=0 # if != 0, then print directly to OFILE
+OFILE="NFPA.mp"
+
+# stdout redirection from Advanced Bash Scripting Guide
+# http://tldp.org/LDP/abs/html/x16355.html#REASSIGNSTDOUT
+if [ $REDIRECT -ne 0 ]
+then
+ exec 6>&1 # Link file descriptor #6 with stdout.
+ # Saves stdout.
+ # stdout replaced with file "logfile.txt".
+ exec > $OFILE
+fi
+
+echo "verbatimtex
+
+\font\bigfont=cmss17 at 50pt
+\font\medfont=cmss17 at 25pt
+\font\smfont=cmss17 at 20pt
+\bigfont
+
+% center two hboxes (containing e.g. \OX and \W)
+\def\vert#1#2{\vbox{\halign{\hfil##\hfil\cr
+\hbox{#1}\cr
+\hbox{#2}\cr}}}
+
+% strikethrough (or strikeout) from Chapter 21 of the TeXbook
+% the height of the strikeout has been raised from 0.8 to 0.9ex
+% to make it look nicer when escaping a purely capital letter (W).
+\def\sout#1{{%
+ \setbox0=\hbox{#1}%
+ \dimen0 0.9ex\dimen1\dimen0\advance\dimen1 by 0.4pt
+ \rlap{\leaders\hrule height \dimen1 depth -\dimen0\hskip\wd0}%
+ \box0
+}}
+
+\def\OX{{OX}}
+\def\W{{\sout{W}}}
+
+etex
+
+%% NFPA values :
+% set in the call to mpost, e.g.
+% $ mpost 'input NFPA.mp; input NFPA_c.mp'
+picture h,f,r,o;
+h := btex $HEALTH etex;
+f := btex $FIRE etex;
+r := btex $REACT etex;"
+
+if [ "$OX" == "y" ] && [ "$W" == "y" ]
+then
+ echo "o := btex {\smfont \vert{\OX}{\W}} etex;"
+elif [ "$OX" == "y" ]
+then
+ echo "o := btex {\medfont \OX} etex;"
+elif [ "$W" == "y" ]
+then
+ echo "o := btex {\medfont \W} etex;"
+fi
+
+if [ $REDIRECT -ne 0 ]
+then
+ # Restore stdout and close file descriptor #6.
+ exec 1>&6 6>&-
+fi
+
+exit 0
--- /dev/null
+\documentclass{article}
+\usepackage{graphicx}
+\usepackage{fullpage}
+
+\DeclareGraphicsRule{*}{mps}{*}{} % if tex doesn't recognize the type, it's mps
+
+% NFPA for 'NFPA.mp image'
+\newcommand{\NFPA}[1]{piezo.#1 \includegraphics{NFPA.#1}\vspace{1cm} \\}
+
+\begin{document}
+\centering
+\NFPA{1}
+\end{document}
--- /dev/null
+#ID Name Amount CAS# Cat# Vendor Recieved Location H F R O M C T Disposed Note
+#ID Name Amount CAS# Catalog # Vendor Date Recieved Location NFPA Health NFPA Fire NFPA Reactivity NFPA Other Mutagen ('M' or '') Carcinogen ('C' or '') Teratogen ('T' or '') Date disposed Note
+0 Acetic Acid 500 ml 64-19-7 A35-500 Fisher 7/5/2001 acidic liquids 3 2 1
+1 Glycine 500 g 56-40-6 15527-013 Life Tech. 1/16/2000 non-hazardous 1 0 0 Avoid strong oxidizers
+2 Sodium Chloride (NaCl) 500 g 7647-14-5 JT4058-1 VWR/Fisher 10/22/2000 non-hazardous 1 0 0
--- /dev/null
+"""
+From pyOpenSSL examples with a bit of wrapping.
+Create certificates and private keys for the 'simple' example.
+"""
+
+from OpenSSL import crypto
+from certgen import * # yes yes, I know, I'm lazy
+
+def get_cert_filenames(server_name) :
+ """
+ Generate private key and certification filesnames.
+ mk_certs(server_name) -> (pkey_filename, cert_filename)
+ """
+ pkey_file = '%s.pkey' % server_name
+ cert_file = '%s.cert' % server_name
+ return (pkey_file, cert_file)
+
+def mk_certs(server_name) :
+ """
+ Generate private key and certification files.
+ mk_certs(server_name) -> (pkey_filename, cert_filename)
+ """
+ pkey_file,cert_file = get_cert_filenames(server_name)
+
+ cakey = createKeyPair(TYPE_RSA, 1024)
+ careq = createCertRequest(cakey, CN='Certificate Authority')
+ cacert = createCertificate(careq, (careq, cakey), 0, (0, 60*60*24*365*5)) # five years
+ open(pkey_file, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, cakey))
+ open(cert_file, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cacert))
+
+if __name__ == "__main__" :
+ import sys
+
+ mk_certs(sys.argv[1])
+
--- /dev/null
+#!/usr/bin/python
+"""
+Simple database-style interface to text-delimited, single files.
+Use this if, for example, your coworkers insist on keeping data compatible with M$ Excel.
+"""
+
+import copy
+from sys import stdin, stdout, stderr
+
+# import os, shutil, and chem_db for managing the database files
+import os, shutil, stat, time
+import os.path
+
+FILE = 'default.db'
+STANDARD_TAB = 8
+
+class fieldError (Exception) :
+ "database fields are not unique"
+ pass
+
+class text_db (object) :
+ """
+ Define a simple database interface for a spread-sheet style database file.
+
+ field_list() : return an ordered list of available fields (fields unique)
+ long_fields() : return an dict of long field names (keyed by field names)
+ record(id) : return a record dict (keyed by field names)
+ records() : return an ordered list of available records.
+ backup() : save a copy of the current database somehow.
+ set_record(i, newvals) : set a record by overwriting any preexisting data
+ : with data from the field-name-keyed dict NEWVALS
+ new_record() : add a blank record (use set_record to change its values)
+ """
+ def __init__(self, filename=FILE, COM_CHAR='#', FS='\t', RS='\n',
+ current_dir='./current/', backup_dir='./backup/' ) :
+ self.filename = filename
+ self.COM_CHAR = COM_CHAR # comment character (also signals header row)
+ self.FS = FS # field seperator
+ self.RS = RS # record seperator
+ self.db_id_field = "db_id" # add a record field for the database index
+ # define directories used by the database
+ self.cur = current_dir
+ self.bak = backup_dir
+
+ if self.filename == None :
+ return # for testing, don't touch the file system
+
+ ## Generate the neccessary directory structure if neccessary
+ for d in [self.cur,self.bak] :
+ self.check_dir(d)
+
+ self._open()
+
+ # directory and file IO operations
+ def check_dir(self. dir) :
+ "Create the database directory if it's missing"
+ if os.path.isdir(dir) :
+ return # all set to go
+ elif os.path.exists(dir) :
+ raise Exception, "Error: a non-directory file exists at %s" % dir
+ else :
+ os.mkdir(dir)
+ def curpath(self) :
+ "Return the path to the current database file."
+ return os.path.join(self.cur, self.filename)
+ def _get_mtime(self) :
+ "Get the timestamp of the last modification to the database."
+ s = os.stat(self.curpath())
+ return s[stat.ST_MTIME]
+ def exists(self) :
+ "Check if the database exists"
+ # for some reason, my system's os.path.exists
+ # returns false for valid symbolic links...
+ return os.path.exists(self.curpath())
+ def _assert_exists(self) :
+ """
+ Assert that the database exists on disk.
+ Print a reasonable error if it does not.
+ """
+ assert self.exists(), "Missing database file %s" % self.curpath()
+ def _open(self) :
+ "Load the database from disk"
+ self._assert_exists()
+ # precedence AND > OR
+ fulltext = file(self.curpath(), 'r').read()
+ self._mtime = self._get_mtime()
+ self._parse(fulltext)
+ def iscurrent(self) :
+ "Check if our memory-space db is still syncd with the disk-space db."
+ return self._mtime == self._get_mtime()
+ def _refresh(self) :
+ "If neccessary, reload the database from disk."
+ if not self.iscurrent() :
+ self._open()
+ def _save(self) :
+ "Create a new database file from a header and list of records."
+ # save the new text
+ fid = file(self.curpath(), 'w')
+ fid.write( self._file_header_string(self._header) )
+ if self._long_header :
+ fid.write( self._file_header_string(self._long_header) )
+ for record in self._records :
+ fid.write( self._file_record_string(record) )
+ fid.close()
+ def backup(self) :
+ "Back up database file"
+ if not self.exists(): return None # nothing to back up.
+ # Append a timestamp to the file & copy to self.bak
+ # the str() builtin ensures a nice, printable string
+ tname = self.filename+'.'+str(int(time.time()))
+ tpath = os.path.join(self.bak, tname)
+ spath = self.curpath()
+ shutil.copy(spath, tpath)
+
+ # file-text to memory operations
+ def _get_header(self, head_line, assert_unique=False,
+ assert_no_db_id_field=True) :
+ """
+ Parse a header line (starts with the comment character COM_CHAR).
+
+ Because doctest doesn't play well with tabs, use colons as field seps.
+ >>> db = text_db(FS=':', filename=None)
+ >>> print db._get_header("#Name:Field 1:ID: another field")
+ ['Name', 'Field 1', 'ID', ' another field']
+ >>> try :
+ ... x = db._get_header("#Name:Field 1:Name: another field", assert_unique=True)
+ ... except fieldError, s :
+ ... print s
+ fields 0 and 2 both 'Name'
+ """
+ assert len(head_line) > 0, 'empty header'
+ assert head_line[0] == self.COM_CHAR, 'bad header: "%s"' % head_line
+ fields = head_line[1:].split(self.FS)
+ if assert_unique :
+ for i in range(len(fields)) :
+ for j in range(i+1,len(fields)) :
+ if fields[i] == fields[j] :
+ raise fieldError, "fields %d and %d both '%s'" \
+ % (i,j,fields[i])
+ if assert_no_db_id_field :
+ for i in range(len(fields)) :
+ if fields[i] == self.db_id_field :
+ raise fieldError, "fields %d uses db_id field '%s'" \
+ % (i,fields[i])
+ return fields
+ def _get_fields(self, line, num_fields=None) :
+ """
+ Parse a record line.
+
+ Because doctest doesn't play well with tabs, use colons as field seps.
+ >>> db = text_db(FS=':', filename=None)
+ >>> print db._get_fields("2-Propanol:4 L:67-63-0:Fisher:6/6/2004",7)
+ ['2-Propanol', '4 L', '67-63-0', 'Fisher', '6/6/2004', '', '']
+ """
+ vals = line.split(self.FS)
+ if num_fields != None :
+ assert len(vals) <= num_fields, "Too many values in '%s'" % line
+ for i in range(len(vals), num_fields) :
+ vals.append('') # pad with empty strings if neccessary
+ return vals
+ def _parse(self, text) :
+ reclines = text.split(self.RS)
+ assert len(reclines) > 0, "Empty database file"
+ self._header = self._get_header(reclines[0], assert_unique=True)
+ self._long_header = None
+ self._records = []
+ if len(reclines) == 1 :
+ return # Only a header
+ # check for a long-header line
+ if reclines[1][0] == self.COM_CHAR :
+ self._long_header = self._get_header(reclines[1])
+ startline = 2
+ else :
+ startline = 1
+ for recline in reclines[startline:] :
+ if len(recline) == 0 :
+ continue # ignore blank lines
+ self._records.append(self._get_fields(recline, len(self._header)))
+
+
+ # memory to file-text operations
+ def _file_record_string(self, record) :
+ """
+ Format record for creating a new database file.
+
+ Because doctest doesn't play well with tabs, use colons as field seps.
+ >>> db = text_db(FS=':', RS=';', filename=None)
+ >>> rs="2-Propanol:4 L:67-63-0:BPA426P-4:Fisher:6/6/2004:2:3:0"
+ >>> print db._file_record_string( db._get_fields(rs)) == (rs+";")
+ True
+ """
+ return "%s%s" % (self.FS.join(record), self.RS)
+ def _file_header_string(self, header) :
+ """
+ Format header for creating a new database file.
+ """
+ return "%s%s%s" % (self.COM_CHAR, self.FS.join(header), self.RS)
+
+
+ # nice, stable api for our users
+ def field_list(self) :
+ "return an ordered list of available fields (fields unique)"
+ return copy.copy(self._header)
+ def long_fields(self) :
+ "return an dict of long field names (keyed by field names)"
+ if self._long_header :
+ return dict(zip(self._header, self._long_header))
+ else : # default to the standard field names
+ return dict(zip(self._header, self._header))
+ def record(self, db_id) :
+ "return a record dict (keyed by field names)"
+ assert type(db_id) == type(1), "id %s not an int!" % str(db_id)
+ assert db_id < len(self._records), "record %d does not exist" % db_id
+ d = dict(zip(self._header, self._records[db_id]))
+ d['db_id'] = db_id
+ return d
+ def records(self) :
+ "return an ordered list of available records."
+ ret = []
+ for id in range(len(self._records)) :
+ ret.append(self.record(id))
+ return ret
+ def len_records(self) :
+ "return len(self.records()), but more efficiently"
+ return len(self._records)
+ def set_record(self, db_id, newvals, backup=True) :
+ """
+ set a record by overwriting any preexisting data
+ with data from the field-name-keyed dict NEWVALS
+ """
+ if backup :
+ self.backup()
+ for k,v in newvals.items() :
+ if k == self.db_id_field :
+ assert int(v) == db_id, \
+ "don't set the db_id field! (attempted %d -> %d)" \
+ % (db_id, int(v))
+ continue
+ # get the index for the specified field
+ assert k in self._header, "unrecognized field '%s'" % k
+ fi = self._header.index(k)
+ # overwrite the record value
+ self._records[db_id][fi] = v
+ self._save()
+ def new_record(self, db_id=None) :
+ """
+ create a blank new record and return it.
+ """
+ record = {}
+ for field in self._header :
+ record[field] = ""
+ record[self.db_id_field] = len(self._records)
+ self._records.append(['']*len(self._header))
+ return record
+
+class indexStringError (Exception) :
+ "invalid index string format"
+ pass
+
+
+class db_pretty_printer (object) :
+ """
+ Define some pretty-print functions for text_db objects.
+ """
+ def __init__(self, db) :
+ self.db = db
+ def _norm_active_fields(self, active_fields_in=None) :
+ """
+ Normalize the active field parameter
+
+ >>> db = text_db(FS=':', RS=' ; ', filename=None)
+ >>> pp = db_pretty_printer(db)
+ >>> db._parse("#Name:Amount:CAS#:Vendor ; 2-Propanol:4 L:67-63-0:Fisher")
+ >>> print pp._norm_active_fields(None) == {'Name':True, 'Amount':True, 'CAS#':True, 'Vendor':True}
+ True
+ >>> print pp._norm_active_fields(['Vendor', 'Amount']) == {'Name':False, 'Amount':True, 'CAS#':False, 'Vendor':True}
+ True
+ >>> print pp._norm_active_fields('1:3') == {'Name':False, 'Amount':True, 'CAS#':True, 'Vendor':False}
+ True
+ """
+ if active_fields_in == None :
+ active_fields = {}
+ for field in self.db.field_list() :
+ active_fields[field] = True
+ return active_fields
+ elif type(active_fields_in) == type('') :
+ active_i = self._istr2ilist(active_fields_in)
+ active_fields = {}
+ fields = self.db.field_list()
+ for i in range(len(fields)) :
+ if i in active_i :
+ active_fields[fields[i]] = True
+ else :
+ active_fields[fields[i]] = False
+ else :
+ if type(active_fields_in) == type([]) :
+ active_fields = {}
+ for field in active_fields_in :
+ active_fields[field] = True
+ elif type(active_fields_in) == type({}) :
+ active_fields = active_fields_in
+ assert type(active_fields) == type({}), 'by this point, should be a dict'
+ for field in self.db.field_list() :
+ if not field in active_fields :
+ active_fields[field] = False
+ return active_fields
+ def full_record_string(self, record, active_fields=None) :
+ """
+ Because doctest doesn't play well with tabs, use colons as field seps.
+ >>> db = text_db(FS=':', RS=' ; ', filename=None)
+ >>> pp = db_pretty_printer(db)
+ >>> db._parse("#Name:Amount:CAS#:Vendor ; 2-Propanol:4 L:67-63-0:Fisher")
+ >>> print pp.full_record_string( db.record(0) ),
+ Name : 2-Propanol
+ Amount : 4 L
+ CAS# : 67-63-0
+ Vendor : Fisher
+ """
+ fields = self.db.field_list()
+ long_fields = self.db.long_fields()
+ active_fields = self._norm_active_fields(active_fields)
+ # scan through and determine the width of the largest field
+ w = 1
+ for field in fields :
+ if active_fields[field] and len(field) > w :
+ w = len(field)
+ # generate the pretty-print string
+ string = ""
+ for field in fields :
+ if field in active_fields and active_fields[field] :
+ string += "%*.*s : %s\n" \
+ % (w, w, long_fields[field], record[field])
+ return string
+ def full_record_string_id(self, id, active_fields=None) :
+ record = self.db.record(id)
+ return self.full_record_string(record, active_fields)
+ def _istr2ilist(self, index_string) :
+ """
+ Generate index lists from assorted string formats.
+
+ Parse index strings
+ >>> pp = db_pretty_printer('dummy')
+ >>> print pp._istr2ilist('0,2,89,4')
+ [0, 2, 89, 4]
+ >>> print pp._istr2ilist('1:6')
+ [1, 2, 3, 4, 5]
+ >>> print pp._istr2ilist('0,3,6:9,2')
+ [0, 3, 6, 7, 8, 2]
+ """
+ ret = []
+ for spl in index_string.split(',') :
+ s = spl.split(':')
+ if len(s) == 1 :
+ ret.append(int(spl))
+ elif len(s) == 2 :
+ for i in range(int(s[0]),int(s[1])) :
+ ret.append(i)
+ else :
+ raise indexStringError, "unrecognized index '%s'" % spl
+ return ret
+ def _norm_width(self, width_in=None, active_fields=None, skinny=True) :
+ "Normalize the width parameter"
+ active_fields = self._norm_active_fields(active_fields)
+ if type(width_in) == type(1) or width_in == 'a' : # constant width
+ width = {} # set all fields to this width
+ for field in active_fields.keys() :
+ width[field] = width_in
+ else :
+ if width_in == None :
+ width_in = {}
+ width = {}
+ for field in active_fields.keys() :
+ # fill in the gaps in the current width
+ if field in width_in :
+ width[field] = width_in[field]
+ else : # field doesn't exist
+ if skinny : # set to a fixed width
+ # -1 to leave room for FS
+ width[field] = STANDARD_TAB-1
+ else : # set to automatic
+ width[field] = 'a'
+ return width
+ def _norm_record_ids(self, record_ids=None) :
+ "Normalize the record_ids parameter"
+ if record_ids == None :
+ record_ids = range(len(self.db.records()))
+ if type(record_ids) == type('') :
+ record_ids = self._istr2ilist(record_ids)
+ stderr.flush()
+ return record_ids
+ def _line_record_string(self, record, width=None, active_fields=None,
+ FS=None, RS=None, TRUNC_STRING=None) :
+ """
+ Because doctest doesn't play well with tabs, use colons as field seps.
+ >>> db = text_db(FS=':', RS=' ; ', filename=None)
+ >>> pp = db_pretty_printer(db)
+ >>> db._parse("#Name:Amount:CAS#:Vendor ; 2-Propanol:4 L:67-63-0:Fisher")
+ >>> print pp._line_record_string_id(0)
+ 2-Propa: 4 L:67-63-0: Fisher ;
+ """
+ fields = self.db.field_list()
+ active_fields = self._norm_active_fields(active_fields)
+ width = self._norm_width(width)
+ if FS == None :
+ FS = self.db.FS
+ if RS == None :
+ RS = self.db.RS
+ for field in fields :
+ if field in active_fields and active_fields[field] :
+ lastfield = field
+ # generate the pretty-print string
+ string = ""
+ for field in fields :
+ if field in active_fields and active_fields[field] :
+ w = width[field]
+ string += "%*.*s" % (w, w, record[field])
+ if field != lastfield :
+ string += "%s" % (FS)
+ string += RS
+ return string
+ def _line_record_string_id(self, id, width=None, active_fields=None,
+ FS=None, RS=None, TRUNC_STRING=None) :
+ return self._line_record_string(self.db.record(id),
+ width, active_fields, FS, RS,
+ TRUNC_STRING)
+ def _get_field_width(self, record_ids, field) :
+ """
+ Return the width of the longest value in FIELD
+ for all the records with db_ids in record_ids.
+ """
+ width = 1
+ for i in record_ids :
+ w = len(self.db.record(i)[field])
+ if w > width :
+ width = w
+ return width
+ def _get_width(self, width_in, active_fields=None, record_ids=None) :
+ """
+ Return the width of the largest value in FIELD
+ for all the records with db_ids in record_ids.
+ """
+ active_fields = self._norm_active_fields(active_fields)
+ width = self._norm_width(width_in, active_fields)
+ record_ids = self._norm_record_ids(record_ids)
+
+ for field in active_fields :
+ if width[field] == 'a' :
+ width[field] = self._get_field_width(record_ids, field)
+ return width
+ def multi_record_string(self, record_ids=None, active_fields=None,
+ width=None, FS=None, RS=None, COM_CHAR=None,
+ TRUNC_STRING=None) :
+ """
+ Because doctest doesn't play well with tabs, use colons as field seps.
+ >>> db = text_db(FS=':', RS=' ; ', filename=None)
+ >>> pp = db_pretty_printer(db)
+ >>> db._parse("#Name:Amount:CAS#:Vendor ; 2-Propanol:4 L:67-63-0:Fisher")
+ >>> print pp.multi_record_string('0'),
+ Name: Amount: CAS#: Vendor ; 2-Propa: 4 L:67-63-0: Fisher ;
+ """
+ if FS == None :
+ FS = self.db.FS
+ if RS == None :
+ RS = self.db.RS
+ if COM_CHAR == None :
+ COM_CHAR = self.db.COM_CHAR
+ active_fields = self._norm_active_fields(active_fields)
+ record_ids = self._norm_record_ids(record_ids)
+ width = self._get_width(width, active_fields, record_ids)
+ # generate the pretty-print string
+ string = ""
+ # print a header line:
+ fields = self.db.field_list()
+ hvals = dict(zip(fields,fields))
+ string += "%s" % self._line_record_string(hvals, width,
+ active_fields, FS, RS,
+ TRUNC_STRING=TRUNC_STRING)
+ # print the records
+ for id in record_ids :
+ string += self._line_record_string_id(id, width,
+ active_fields, FS, RS,
+ TRUNC_STRING=TRUNC_STRING)
+ return string
+
+
+def _test():
+ import doctest
+ doctest.testmod()
+
+if __name__ == "__main__" :
+ _test()