1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This file is part of ChemDB.
5 # ChemDB is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
10 # ChemDB is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with ChemDB. If not, see <http://www.gnu.org/licenses/>.
18 """Chemical inventory web interface.
21 from __future__ import with_statement
23 from cgi import escape
30 from jinja2 import Environment, FileSystemLoader
32 from .daemon import Daemon
33 from .db.text import TextDB
34 from .chemdb import valid_CASno, MSDSManager, DocGen
40 class Server (object):
41 """ChemDB web interface."""
42 def __init__(self, db=None, MSDS_manager=None, docs=None,
43 template_root='template', static_dir='static',
46 self.MSDS_manager = MSDS_manager
48 self.static_dir = static_dir
49 self.static_url = static_url
50 self.env = Environment(loader=FileSystemLoader(template_root))
51 self.width_max = {'CAS#':12}
52 self.entry_width = 50 # display width of entry fields
53 self.raw_fields = ['ID','Name']
54 self.MSDS_content_types = [
55 'text/plain', 'text/html', 'application/pdf']
58 def index(self, id=None):
59 """Chemical index page.
63 return self._index_page()
65 return self._view_page(id=id)
67 def _index_page(self):
68 records = self.db.records()
69 for record in records:
70 for field in self.db.field_list():
71 rstring = record[field]
72 if field in self.width_max: # truncate if width is regulated...
73 w = self.width_max[field]
74 if len(rstring) > w: # ... and full string is too long.
75 rstring = "%s..." % rstring[:w-3]
76 # if the field isn't raw, protect special chars
77 if not field in self.raw_fields:
78 rstring = escape(rstring)
79 if self.MSDS_manager.has_MSDS(record['db_id']):
80 # link the id to the MSDS
83 % (self._MSDS_path(record['db_id']), record['Name']))
84 template = self.env.get_template('index.html')
85 return template.render(fields=self.db.field_list(), records=records)
87 def _view_page(self, id):
89 record = self.db.record(db_id)
91 if self.MSDS_manager.has_MSDS(db_id):
92 path = self._MSDS_path(db_id)
93 MSDS_link = '<a href="%s">%s</a>' % (
94 path, os.path.basename(path))
95 template = self.env.get_template('record.html')
96 return template.render(fields=self.db.field_list(),
97 long_fields=self.db.long_fields(),
102 def _MSDS_path(self, db_id):
103 path = self.MSDS_manager.get_MSDS_path(db_id)
104 path = os.path.relpath(path, self.static_dir)
105 path = os.path.join(self.static_url, path)
109 def edit(self, id=None, **kwargs):
110 """Render the edit-record.html template for the specified record.
114 self._update_record(id=id, **kwargs)
115 raise cherrypy.HTTPRedirect(u'.?id=%s' % id, status=303)
116 MSDSs = self.MSDS_manager.get_all(simlinks=False)
117 if id in [None, '-1']:
118 record = dict([(field,'') for field in self.db.field_list()])
119 record['db_id'] = '-1'
120 record['ID'] = str(record['db_id'])
123 record = self.db.record(db_id)
124 if self.MSDS_manager.has_MSDS(db_id):
125 MSDSs = None # only ask for an MSDS if we still need one
126 template = self.env.get_template('edit-record.html')
127 return template.render(fields=self.db.field_list(),
128 long_fields=self.db.long_fields(),
131 entry_width=self.entry_width,
134 def _update_record(self, id=None, **kwargs):
135 """Update a record with form input.
138 if id in [None, '-1']:
139 record = self.db.new_record()
140 logging.info('new record %s' % record['db_id'])
143 record = self.db.record(db_id)
145 for field in self.db.field_list():
146 if kwargs[field] != record[field]:
147 # TODO: add validation!
149 record[field] = kwargs[field]
150 if kwargs.get('MSDS source', None) == 'upload':
151 # Handle any MSDS file actions
152 f = kwargs['MSDS upload']
153 contents = f.file.read()
154 if len(contents) > 0 and f.type in self.MSDS_content_types:
155 self.MSDS_manager.save(int(record['ID']), contents, f.type)
156 elif kwargs.get('MSDS source', None) == 'share':
157 logging.info('linking MSDS %d to %d'
158 % (int(record['ID']),
159 int(kwargs['MSDS share'])) )
160 self.MSDS_manager.link(int(record['ID']),
161 int(kwargs['MSDS share']))
163 self.db.set_record(record['db_id'], record, backup=True)
167 "Generate and serve assorted official documents."
168 def __init__(self, db=None, docgen=None, template_root='template'):
172 self.env = Environment(loader=FileSystemLoader(template_root))
176 """List the available documents.
178 template = self.env.get_template('docs.html')
179 return template.render()
182 def inventory_pdf(self):
184 path = self.docgen.inventory(namewidth=self.namewidth)
185 cherrypy.response.headers['Content-Type'] = 'application/pdf'
186 return file(path, 'rb').read()
189 def door_warning_pdf(self, location=None):
192 valid = lambda r: r['Disposed'] == ''
194 regexp = re.compile(location, re.I) # Case insensitive
195 valid = lambda r: r['Disposed'] == '' and regexp.match(r['Location'])
196 path = self.docgen.door_warning(valid_record=valid)
197 cherrypy.response.headers['Content-Type'] = 'application/pdf'
198 return file(path, 'rb').read()
201 class ServerDaemon (Daemon):
203 super(ServerDaemon, self).__init__()
206 self.pidfile = './chem_web.pid'
207 self.logfile = './chem_web.log'
208 self.loglevel = 'INFO'
211 if cherrypy.__version__.startswith('3.'):
212 cherrypy.quickstart(root=self.server, config=self.app_config)
214 # if self.pkey_file == None or self.cert_file == None:
215 # logging.info("http://%s:%d/" % web.validip(self.ip_address))
217 # logging.info("https://%s:%d/" % web.validip(self.ip_address))
219 # webpy_func = web.webpyfunc(urls, globals(), False)
220 # wsgi_func = web.wsgifunc(webpy_func)
221 # web.httpserver.runsimple(wsgi_func,
222 # web.validip(self.ip_address),
223 # ssl_private_key_filename=self.pkey_file,
224 # ssl_certificate_filename=self.cert_file)
226 def read_basic_config(self):
229 def parse_options(self):
230 from optparse import OptionParser
232 usage_string = ('%prog [options]\n'
234 '2008, W. Trevor King.\n')
235 version_string = '%%prog %s' % __version__
236 p = OptionParser(usage=usage_string, version=version_string)
239 p.add_option('-t', '--test', dest='test', default=False,
240 action='store_true', help='Run internal tests and exit.')
243 p.add_option('--start', dest='action',
244 action='store_const', const='start', default='start',
245 help='Start the daemon (the default action).')
246 p.add_option('--stop', dest='action',
247 action='store_const', const='stop', default='start',
248 help='Stop the daemon.')
249 p.add_option('-n', '--nodaemon', dest='daemonize',
250 action='store_false', default=True,
251 help='Run in the foreground.')
254 p.add_option('-a', '--address', dest='address', default='127.0.0.1',
256 help='Address that the server will bind to (%default).')
257 p.add_option('-p', '--port', dest='port', default='8080',
259 help='Port that the server will listen on (%default).')
260 p.add_option('-s', '--secure', dest="secure",
261 help="Run in secure (HTTPS) mode.",
262 type='string', metavar="PKEY_FILE:CERT_FILE")
263 p.add_option('-v', '--verbose', dest="verbose", action="store_true",
264 help="Print lots of debugging information.",
266 p.add_option('--static', dest='static', metavar='PATH',
267 help="Path to the directory of static files (%default).",
268 default=os.path.join('example', 'static'))
269 p.add_option('--template', dest='template', metavar='PATH',
270 help="Path to the directory of template files (%default).",
271 default=os.path.join('template', 'web'))
272 p.add_option('--doc', dest='doc', metavar='PATH',
273 help="Path to the directory of document generation files (%default).",
274 default=os.path.join('template', 'doc'))
275 p.add_option('--htaccess', dest='htaccess', metavar='FILE',
276 help="Path to the htaccess file (%default).",
280 p.add_option('--database', dest='database', metavar='FILE',
281 help="Path to the database file (%default).",
282 default=os.path.join('example', 'inventory.db'))
284 self.options, args = p.parse_args()
287 if self.options.test == True:
291 if self.options.verbose:
292 self.loglevel = 'DEBUG'
294 # get self.options for httpserver
295 if self.options.secure != None:
296 split = self.options.secure.split(':', 1)
297 assert len(split) == 2, (
298 "Invalid secure argument '%s'" % self.options.secure)
299 self.pkey_file = split[0]
300 self.cert_file = split[1]
302 # HACK! to ensure we *always* get utf-8 output
304 #sys.setdefaultencoding('utf-8')
306 dirname,filename = os.path.split(
307 os.path.abspath(self.options.database))
308 static_dir = os.path.abspath(self.options.static)
309 MSDS_dir = os.path.join(static_dir, 'MSDS')
310 template_dir = os.path.abspath(self.options.template)
311 doc_dir = os.path.abspath(self.options.doc)
312 db = TextDB(filename=filename, current_dir=dirname)
313 MSDS_manager = MSDSManager(db=db, dir=MSDS_dir)
314 docgen = DocGen(db=db, doc_root=doc_dir)
315 docs = Docs(db=db, docgen=docgen, template_root=template_dir)
316 server = Server(db=db, MSDS_manager=MSDS_manager, docs=docs,
317 template_root=template_dir, static_dir=static_dir,
318 static_url='/static')
320 if cherrypy.__version__.startswith('3.'):
321 cherrypy.config.update({ # http://www.cherrypy.org/wiki/ConfigAPI
322 'server.socket_host': self.options.address,
323 'server.socket_port': int(self.options.port),
324 'tools.decode.on': True,
325 'tools.encode.on': True,
326 'tools.encode.encoding': 'utf8',
327 'tools.staticdir.root': static_dir,
330 if cherrypy.__version__.startswith('3.2'):
332 get_ha1 = cherrypy.lib.auth_digest.get_ha1_file_htdigest(
333 self.options.htaccess)
338 'tools.auth_digest.on': True,
339 'tools.auth_digest.realm': 'cookbook',
340 'tools.auth_digest.get_ha1': get_ha1,
341 'tools.auth_digest.key': str(uuid.uuid4()),
346 with open(self.options.htaccess, 'r') as f:
348 user,realm,ha1 = line.strip().split(':')
349 passwds[user] = ha1 # use the ha1 as the password
354 'tools.digest_auth.on': True,
355 'tools.digest_auth.realm': 'cookbook',
356 'tools.digest_auth.users': passwds,
360 'tools.staticdir.on': True,
361 'tools.staticdir.dir': '',
364 if digest_auth != None:
366 app_config[url] = digest_auth
368 self.app_config = app_config
370 raise NotImplementedError(
371 'Unsupported CherryPy version %s' % cherrypy.__version__)