3 # Web application for posting additions/modifications to the chem-inventory
4 # derived from simplewiki and wiki_py, by Adam Bachman
5 # ( http://bachman.infogami.com/ )
6 # Authentication following Snakelets
7 # http://snakelets.sourceforge.net/manual/authorization.html
9 # Remember to delete the templates directory after making alterations,
10 # because this program only generates templates if they are missing.
13 ## Bring in some useful goodies:
14 # web, because we're running a web server, and generating html;
15 # time, for timestamping backups;
16 # os, for creating the template directories; and
17 # re, for replacing internal node links with proper markup
19 # md5, for hashing passwords
22 # database must support the following methods :
23 # field_list() : return an ordered list of available fields (fields unique)
24 # long_field() : return an dict of long field names (keyed by field names)
25 # record(id) : return a record dict (keyed by field names)
26 # records() : return an ordered list of available records.
27 # backup() : save a copy of the current database somehow.
28 # set_record(i, newvals) : set a record by overwriting any preexisting data
29 # : with data from the field-name-keyed dict NEWVALS
31 database = text_db.text_db
32 from chem_db import valid_CASno, MSDS_manager, docgen
33 DB_FILE = 'inventory.db'
34 import sys, daemon, logging
35 PID_FILE = './chem_web.pid'
36 LOG_FILE = './chem_web.log'
38 ## Also import a markup language, and associated regexp functions
39 from markdown import markdown
40 ## for converting the first character in record names, and stripping |
41 from string import upper, rstrip
46 from mk_simple_certs import get_cert_filenames
47 server_name = 'chem_web'
51 ## For web.py, when a URL matches '/view', handle with class 'view'
52 # when it matches '/edit', handle with class 'edit',
53 # and when it matches '/login', handle with class 'login',
54 # Note, there are no .html extension.
56 # link to edit pages using db_id
57 # link to MSDSs with ID
58 urls = ('/(view)?', 'view',
59 '/edit/([0-9]+)', 'edit',
60 '/MSDS/([0-9]+)', 'MSDS',
61 '/docs/([a-z._]*)', 'docs',
64 # limit the display width of selected fields
65 WIDTH_MAX = {'CAS#':12}
67 # set the display width of entry fields
72 # when the server recieves the command "GET '/view', call view.get('view')
75 Render the view.html template using name and the previous text.
76 If the file doesn't exist, use default text '%s doesn't exist'.
82 for record in db.records() :
83 id = int(record['ID'])
84 record['ID'] = '<a href="/edit/%d">%s</a>' \
86 if MSDSman.has_MSDS(id) : # link the id to the MSDS
87 record['Name'] = '<a href="/MSDS/%d">%s</a>' \
88 % (record['db_id'], record['Name'])
90 print render.view('Chemical inventory',
91 db.len_records(), db.field_list(), recs)
94 "Provide methods for handling requests related to edit pages"
97 Render the edit.html template using the specified record number.
101 if db_id >= db.len_records() :
102 logging.info("new record")
103 assert db_id == db.len_records(), \
104 'invalid id: %d (max %d)' % (db_id, db.len_records())
105 record = db.new_record()
106 record['ID'] = str(record['db_id'])
107 db.set_record(db_id, record)
108 if MSDSman.has_MSDS(db_id) :
111 # only ask for an MSDS if we still need one
112 MSDSs = MSDSman.get_all(simlinks=False)
113 print render.edit(int(name), db.field_list(),
114 db.long_fields(), db.record(int(name)), MSDSs)
115 def POST(self, name):
117 Read the form input and update the database accordingly.
118 Then redirect to /view.
121 record = db.record(int(name))
122 ## Generate a new record from the form input
123 # 'MSDS={}' to storify MSDS as a FieldStorage (dict-like) instance,
124 # otherwise it's saved as a string, and we loose info about it.
125 # e.g. newvals['MSDS'].type = 'text/html'
126 # The contents of the file are saved to newvals['MSDS'].value
127 newvals = web.input(MSDS={})
129 for field in db.field_list() :
130 if newvals[field] != record[field] :
132 record[field] = newvals[field]
133 if 'MSDS source' in newvals :
134 # Handle any MSDS file actions
135 if newvals['MSDS source'] == 'upload' :
136 if len(newvals['MSDS'].filename) > 0 and len(newvals['MSDS'].value) > 0:
137 # only save if there is a file there to save ;)
138 #print >> stderr, web.ctx.env['CONTENT_TYPE']
139 #print >> stderr, web.ctx.keys()
140 #print >> stderr, dir(newvals['MSDS'])
141 #print >> stderr, type(newvals['MSDS'])
142 #print >> stderr, newvals['MSDS'].filename
143 #print >> stderr, newvals['MSDS'].type
144 MSDSman.save(int(name), newvals['MSDS'].value,
145 newvals['MSDS'].type)
147 logging.info('linking MSDS %d to %d' \
148 % (int(record['ID']),
149 int(newvals['MSDS share'])) )
150 MSDSman.link(int(record['ID']),
151 int(newvals['MSDS share']))
153 db.set_record(int(name), record, backup=True)
154 # redirect to view all
155 web.seeother('/view')
157 class MSDS (object) :
158 "Serve MSDS files by ID"
160 "Serve MSDS files by ID"
162 if MSDSman.has_MSDS(id) :
163 mime = MSDSman.get_MSDS_MIME(id)
164 web.header("Content-Type", mime)
165 print file(MSDSman.get_MSDS_path(id), 'rb').read() ,
167 print render.error(id)
169 class docs (object) :
170 "Generate and serve assorted official documents"
173 List the available documents.
178 if name == 'inventory.pdf' :
179 path = dgen.inventory(namewidth=40)
180 print file(path, 'rb').read() ,
181 if name == 'door_warning.pdf' :
182 path = dgen.door_warning()
183 print file(path, 'rb').read() ,
186 class login (object) :
187 "Print an alphabetized index of all existing pages"
189 print "Not yet implemented"
190 #print render.stat('login')
192 class markup (object) :
193 "Convert text to html, using Markdown with a bit of preformatting"
195 # [[optional display text|name with or_without_spaces]]
201 self.linkexp = re.compile(linkregexp)
202 self.linebreak = ' \n'
203 self.uscore = re.compile('_')
204 self.space = re.compile(' ')
205 def htmlize(self, text) :
207 pre_markup = self._preformat(text)
208 #print "Preformatted '"+pre_markup+"'"
209 return self._markup( pre_markup )
210 def _markup(self, text) :
211 # markdown() implements the text->html Markdown semantics
212 # the str() builtin ensures a nice, printable string
213 return str(markdown(text))
214 def _preformat(self, text) :
215 #print "Preformatting '"+text+"'"
216 return self._autolink(text)
217 def _autolink(self, text) :
218 "Replace linkexps in text with Markdown syntax versions"
219 # sub passes a match object to link2markup for each match
220 # see http://docs.python.org/lib/match-objects.html
221 return self.linkexp.sub(self.matchlink2markup, text)
222 def matchlink2markup(self, match) :
223 # The name is the first (and only) matched region
224 #print match.groups()
225 name = match.groups()[1]
226 text = match.groups()[0]
227 return self.link2markup(text, name)
228 def linklist(self, text) :
229 linklist = [] # empty list
230 for m in self.linkexp.finditer(text) :
231 linklist.append( self.str2name(m.groups()[1]) )
234 def link2markup(self, text, name) :
236 usage: string = link2markup(text, target)
237 takes the string name of a wiki page,
238 and returns a string that will be markuped into a link to that page
239 displaying the text text.
241 # convert to Markdown pretty link syntax
242 if text : # remove trailing |
243 text = text.rstrip('|')
245 text = self.name2str(name)
246 name = self.str2name(name)
247 return str('['+text+'](/'+name+')')
248 def name2str(self, name) :
250 usage: string = name2str(name)
251 takes the string name of a wiki page,
252 and converts it to display format
255 w_spaces = self.uscore.sub(' ', name)
257 def str2name(self, string) :
259 usage: string = name2str(name)
260 Converts strings to the relevent wiki page name.
263 wo_spaces = self.space.sub('_', string)
264 cap_first = upper(wo_spaces[0]) + wo_spaces[1:]
266 def make_add_button(self, next_db_id) :
267 string = '<form action="/edit/%d" method="get">\n' % next_db_id
268 string += ' <input type="submit" value="Add entry">\n'
269 string += '</form><br>\n'
271 def make_table(self, fields, records, width_max=WIDTH_MAX,
274 >>> print make_table(['A','B','C'],[{'A':'a','B':'b'},{'A':'d','B':'e','C':'f'}]),
276 <tr><td><b>A</b></td><td><b>B</b></td><td><b>C</b></td></tr>
277 <tr><td>a</td><td>b</td><td></td></tr>
278 <tr><td>d</td><td>e</td><td>f</td></tr>
285 for field in fields :
286 string += '<td><b>%s<b></td>' % self.htmlize(field)
289 for record in records :
291 for field in fields :
292 rstring = record[field]
293 # truncate if the width is regulated...
294 if field in width_max :
296 # ... and the full string is too long
297 if len(rstring) > w :
298 rstring = "%s..." % rstring[:w]
299 # if the field isn't raw, protect special chars
300 if not field in raw_fields :
301 rstring = self.htmlize(rstring)
302 string += '<td>%s</td>' % rstring
305 string += '</table>\n'
307 def record_form_entries(self, index, fields, lfields, record, MSDSs=None, entry_width=ENTRY_WIDTH):
309 >>> print record_form_entries(4,['A','B','MSDS'],{'A':'AA','B':'BB','MSDS'},{'A':'a','B':'b'}),
311 <tr><td><label for="A">AA</label></td><td>:</td>
312 <td><input type="text" size="50" id="A" name="A" value="a"></td></tr>
313 <tr><td><label for="B">BB</label></td><td>:</td>
314 <td><input type="text" size="50" id="B" name="B" value="b"></td></tr>
315 <tr><td><label for="MSDS">MSDS</label></td><td>:</td>
316 <td><input type="file" size="50" id="MSDS" name="MSDS" value=""></td></tr>
321 # add the record fields
322 for field in fields :
323 # get the record string
324 rstring = self.htmlize(record[field])
325 fstring = self.htmlize(field)
326 lfstring = self.htmlize(lfields[field])
327 string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' % (fstring, lfstring)
328 string += ' <td><input type="text" size="%d" id="%s" name="%s" value="%s"></td></tr>\n' \
329 % (entry_width, fstring, fstring, rstring)
331 ## add an MSDS radio, share menu, and file upload fields
333 lfstring = fstring = 'MSDS source'
334 string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' \
335 % (fstring, lfstring)
336 string += ' <td><input type="radio" id="%s" name="%s" value="upload" checked>Upload\n' \
338 string += ' <input type="radio" id="%s" name="%s" value="share">Share</td></tr>\n' \
343 lfstring = 'Upload MSDS'
344 string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' \
345 % (fstring, lfstring)
346 string += ' <td><input type="file" size="%d" id="%s" name="%s" accept="text/plain,text/html,application/pdf" value=""></td></tr>\n' \
347 % (entry_width, fstring, fstring)
350 fstring = 'MSDS share'
351 lfstring = 'Use an already uploaded MSDS'
352 string += ' <tr><td><label for="%s">%s</label></td><td>:</td>\n' \
353 % (fstring, lfstring)
354 string += ' <td><select size="1" id="%s" name="%s">\n' \
357 lfstring = self.htmlize(MSDS['Name'])
359 string += ' <option value="%d">%d: %s</option>\n' \
361 string += ' </select></td></tr>\n'
363 string += '</table><br>\n'
366 class templt (object) :
367 "Handle some template wrapping"
369 self.dir = 'templates/'
370 if not os.path.exists(self.dir): os.mkdir(self.dir)
371 ## Write simple template html pages if they don't already exist.
372 # see ( http://webpy.org/templetor ) for the syntax.
373 html=('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"\n'
374 ' "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n'
375 '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n')
376 if not os.path.exists(self.dir+'view.html') :
377 view=('$def with (name, next_db_id, fields, records)\n'
381 ' $:name2str(name) \n'
386 ' $:name2str(name)\n'
389 ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm">the rules</a>\n'
390 ' for more information.\n'
391 ' See the <a href="/docs/">docs</a> page to generate required documents.<br>\n'
393 ' $:make_add_button(next_db_id)\n'
394 ' $:make_table(fields, records)\n'
395 ' $:make_add_button(next_db_id)\n'
398 file(self.dir+'view.html', 'w').write(view)
399 if not os.path.exists(self.dir+'edit.html') :
401 # the form encoding type 'enctype="multipart/form-data">\n'
402 # is only required because of the MSDS file-upload field.
403 edit=('$def with (index, fields, lfields, record, MSDSs)\n'
407 ' $:htmlize(record["Name"]) \n'
411 ' <a style="cursor:pointer;" onclick="history.back()"><h1>\n'
412 ' Editing: $:htmlize(record["Name"]) \n'
414 ' <form action="" method="post" enctype="multipart/form-data">\n'
415 ' $:record_form(index, fields, lfields, record, MSDSs)<br />\n'
416 ' <input type="submit" value="Submit" />\n'
420 file(self.dir+'edit.html', 'w').write(edit)
421 if not os.path.exists(self.dir+'error.html') :
422 # like view, but no edit ability
423 # since the content is generated,
424 # or backlink list, since there would be so many.
425 stat=('$def with (id)\n'
431 ' There is currently no MSDS file for \n'
435 file(self.dir+'error.html', 'w').write(stat)
436 if not os.path.exists(self.dir+'docs.html') :
441 ' <a href="/docs/inventory.pdf">Inventory</a>\n'
442 ' in accordance with the \n'
443 ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">\n'
444 ' Chemical Hygiene Plan Section E-7</a>.<br>\n'
445 ' <a href="/docs/door_warning.pdf">Door warning</a>\n'
446 ' in accordance with the Chemical Hygiene Plan Sections\n'
447 ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">E-7</a>\n'
449 ' <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe10">E-10</a>\n'
453 file(self.dir+'docs.html', 'w').write(docs)
456 class chem_web_daemon (daemon.Daemon) :
463 if self.pkey_file == None or self.cert_file == None :
464 logging.info("http://%s:%d/" % web.validip(self.options.ip))
466 logging.info("https://%s:%d/" % web.validip(self.options.ip))
468 ## web.py should give detailed error messages, not just `internal error'
469 web.internalerror = web.debugerror
471 ## How we'd start webpy server if we didn't need command line args or SSL
472 # Pass web my URL regexps, and the globals from this script
473 # You can also pass it web.profiler to help optimize for speed
474 #web.run(urls, globals())
475 ## How we have to start it now
476 webpy_func = web.webpyfunc(urls, globals(), False)
477 wsgi_func = web.wsgifunc(webpy_func)
478 web.httpserver.runsecure(wsgi_func,
479 web.validip(self.options.ip),
480 ssl_private_key_filename=self.pkey_file,
481 ssl_certificate_filename=self.cert_file)
483 def read_basic_config(self) :
485 def parse_options(self):
486 from optparse import OptionParser
488 usage_string = ('%prog [options]\n'
490 '2008, W. Trevor King.\n')
491 version_string = '%%prog %s' % __version__
492 parser = OptionParser(usage=usage_string, version=version_string)
495 parser.add_option('--start', dest='action',
496 action='store_const', const='start', default='start',
497 help='Start the daemon (the default action)')
498 parser.add_option('--stop', dest='action',
499 action='store_const', const='stop', default='start',
500 help='Stop the daemon')
501 parser.add_option('-n', '--nodaemon', dest='daemonize',
502 action='store_false', default=True,
503 help='Run in the foreground')
506 parser.add_option('-a', '--ip-address', dest="ip",
507 help="IP address (default '%default')",
508 type='string', metavar="IP", default='0.0.0.0:8080')
509 parser.add_option('-s', '--secure', dest="secure",
510 help="Run in secure (HTTPS) mode.",
511 type='string', metavar="PKEY_FILE:CERT_FILE")
512 parser.add_option('-v', '--verbose', dest="verbose", action="store_true",
513 help="Print lots of debugging information",
516 self.options, self.args = parser.parse_args()
519 if self.options.verbose :
520 self.loglevel = 'DEBUG'
521 # get options for httpserver
522 if self.options.secure != None :
523 split = self.options.secure.split(':')
524 assert len(split) == 2, "Invalid secure argument '%s'"
525 self.pkey_file = split[0]
526 self.cert_file = split[1]
528 self.pkey_file = None
529 self.cert_file = None
531 ### the following instances and definitions must have
532 ## global scope. because they are called from inside web
533 # create the database
534 db = database(filename=DB_FILE)
535 MSDSman = MSDS_manager(db)
538 ## set up the templates
540 # Tell templator where to look for templates
541 # to provide a framework for the generated webpages
542 render = web.template.render(tmplt.dir)
544 ## Define markup functions
546 htmlize = lambda t : mkup.htmlize(t)
547 name2str = lambda n : mkup.name2str(n)
548 mktable = lambda h,r : mkup.make_table(h,r,WIDTH_MAX,raw_fields=('ID','Name'))
549 mkadd = lambda ni : mkup.make_add_button(ni)
550 record_form = lambda i,f,l,r,M : mkup.record_form_entries(i,f,l,r,M)
551 ## Give templates access to the htmlize, make_table, and name2str functions
552 # ( see http://webpy.org/templetor and our view.html )
553 web.template.Template.globals['htmlize'] = htmlize
554 web.template.Template.globals['make_table'] = mktable
555 web.template.Template.globals['make_add_button'] = mkadd
556 web.template.Template.globals['name2str'] = name2str
557 web.template.Template.globals['record_form'] = record_form
560 ## If this script is run from the command line,
561 # use the tools we've just defined to host the chemical inventory
562 if __name__=="__main__":
563 chem_web_daemon().main()