+#!/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()