*.pyc
-chem_web.log*
-backup/
+build
-Web, python, and command-line interfaces for managing a chemical inventory.
+.. -*- coding: utf-8 -*-
-Commands
-========
+Chemdb is a chemical inventory system written in Python_ using the
+Django_ framework. It makes it easy for us to keep track of what we
+have in the lab. It also generates the required safety information
+automatically (door warnings and inventories).
-The web interface (:file:`bin/chem_web.py`) is a web-daemon using
-CherryPy. Standard command for starting the daemon::
+Installation
+============
- $ bin/chem_web.py -a 192.168.1.2 -p 55555
+Packages
+--------
+
+Gentoo
+~~~~~~
+
+I've packaged chemdb for Gentoo_. You need layman_ and my `wtk
+overlay`_. Install with::
+
+ # emerge -av app-portage/layman
+ # layman --add wtk
+ # emerge -av dev-python/chemdb
+
+Dependencies
+------------
+
+If you're installing by hand or packaging calibcant for another
+distribution, you'll need the following dependencies:
+
+=========== ================= ===================================
+Package Debian_ Gentoo_
+=========== ================= ===================================
+Django_ python-django dev-python/django
+Grappelli_ dev-python/django-grappelli [#wtk]_
+=========== ================= ===================================
+
+.. [#wtk] In the `wtk overlay`_.
+
+Installing by hand
+------------------
+
+Chemdb is available as a Git_ repository::
+
+ $ git clone git://tremily.us/chemdb.git
+
+See the homepage_ for details. To install the checkout, run the
+standard::
+
+ $ python setup.py install
+
+Usage
+=====
+
+Setup
+-----
+
+If you don't have a Django project and you just want to run chemdb as
+a stand-alone service, you can use the example project written up in
+``example``. Set up the project (once)::
-Standard command for stopping the daemon::
+ $ python example/manage.py syncdb
- $ bin/chem_web.py --stop
+See the `Django documentation`_ for more details.
-From the command line, you can validate CAS#s (and possibly other fields) with::
+You may also want to load some example data, to make your initial
+browsing more interesting::
- $ bin/chem_db.py -f example/inventory.db -V
+ $ python example/manage.py loaddata example_data
+Running
+-------
-Database format
+Run the app on your local host (as many times as you like)::
+
+ $ python example/manage.py runserver
+
+You may need to add the current directory to ``PYTHONPATH`` so
+``python`` can find the ``chemdb`` package. If you're running
+``bash``, that will look like::
+
+ $ PYTHONPATH=".:$PYTHONPATH" python example/manage.py runserver
+
+Hacking
+=======
+
+This project was largely build following the `Django tutorial`_.
+That's a good place to start if you're new to Django.
+
+Other resources
===============
-:mod:`chemdb.db.text` 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 the
-ChemDB interface. A brief example :file:`inventory.db` file is
-included in the examples directory.
+You can `search CAS Registry numbers`_ at NIST. This is useful for
+decoding MSDS information.
-* 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.
+NFPA fire diamond
+-----------------
-The fields H, F, R, and S are the NFPA Health, Fire, Reactivity, and
-Special Hazards (NFPA diamond).
+These are the meanings of the various NFPA warnings:
* 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
+
+ 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..)
-* Unofficial Special Hazard codes:
- * ACID acid
- * ALK base
- * COR corrosive
- * BIO Biohazard
- * POI Poison
- * CRY Cyrogenic
-
-
-Contents
-========
-
- AUTHORS (list of authors contributing code to ChemDB)
- bin (executibles)
- |-- chem_db.py (command line interface)
- `-- chem_web.py (web interface)
- chemdb (python package)
- contrib (useful but non-critical utilities)
- `-- ssl (SSL key/certificate generation)
- COPYING (GNU GPLv3 license)
- DEPENDENCIES (description of software dependencies)
- example (example data for demonstrations and testing)
- |-- inventory.db (example inventory database)
- `-- static (example directory for statically served content)
- `-- MSDS (example MSDS storage location)
- `-- 0.html (dummy MSDS for acetic acid)
- README (this file)
- template
- |-- doc (LaTeX source for PDF generation)
- `-- web (Jinja2 templates for HTML generation)
- .update-opyright.conf (configuration for copyright blurb maintenance)
-
-
-Generated files and directories
--------------------------------
-
-============ =======================================================================
-backup store database file snapshots from every change
-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.
+ 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)
-History
-=======
+* Reactivity hazards:
-The original web fron-end was based on Adam Bachman's simplewiki.py_.
+ 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
-.. simplewiki.py: http://bachman.infogami.com/another_simple_wiki
+* Special Hazards have the following codes:
+ * OX strong oxidizer
+ * W̶ water reactive
+ * SA simple asphyxiants (The only gases for which this symbol is
+ permitted are nitrogen, helium, neon, argon, krypton, and xenon.)
-License
+Licence
=======
-ChemDB is released under the GNU General Public License version 3.
-See :file:`COPYING` for details.
+This project is distributed under the `GNU General Public License
+Version 3`_ or greater.
+
+Author
+======
+
+W. Trevor King
+wking@drexel.edu
+
+.. _Python: http://www.python.org/
+.. _Django: https://www.djangoproject.com/
+.. _Gentoo: http://www.gentoo.org/
+.. _layman: http://layman.sourceforge.net/
+.. _wtk overlay: http://blog.tremily.us/posts/Gentoo_overlay/
+.. _Debian: http://www.debian.org/
+.. _Grappelli: https://github.com/sehmaschine/django-grappelli
+.. _Git: http://git-scm.com/
+.. _homepage: http://blog.tremily.us/posts/ChemDB/
+.. _Django documentation: https://docs.djangoproject.com/
+.. _Django tutorial: https://docs.djangoproject.com/en/1.3/intro/tutorial01/
+.. _search CAS Registry numbers:
+ http://webbook.nist.gov/chemistry/casf-ser.html
+.. _GNU General Public License Version 3: http://www.gnu.org/licenses/gpl.txt
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of ChemDB.
-#
-# ChemDB is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# ChemDB is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ChemDB. If not, see <http://www.gnu.org/licenses/>.
+# Copyright
+
+import logging as _logging
+import logging.handlers as _logging_handlers
+
+
+__version__ = '0.5'
+
+
+LOG = _logging.getLogger('chemdb')
+LOG.setLevel(_logging.ERROR)
+LOG.addHandler(_logging_handlers.SysLogHandler(address='/dev/log'))
--- /dev/null
+# Copyright
+
+from django import forms as _forms
+from django.contrib import admin as _admin
+from django.db import models as _django_models
+
+from . import models as _models
+
+
+#class ChemicalInline (_admin.TabularInline):
+# model = _models.Chemical
+# extra = 0
+#
+#
+#class IngredientBlockAdmin (_admin.ModelAdmin):
+# fieldsets = [
+# (None, {'fields': ['name']}),
+# ('Directions', {'fields': ['directions_markdown'],
+# 'classes': ['collapse']}),
+# ]
+# inlines = [IngredientInline]
+#
+# list_display = ['name', 'recipe']
+# extra = 0
+#
+#
+#class IngredientBlockInline (admin.TabularInline):
+# model = models.IngredientBlock
+# fieldsets = [
+# (None, {'fields': ['name', 'position']}),
+# ]
+# sortable_field_name = 'position'
+# inlines = [IngredientInline]
+# list_display = ['name']
+# extra = 0
+# show_edit_link = True # https://code.djangoproject.com/ticket/13163
+# # work around 13163
+# #template = 'admin/edit_inline/tabular-13163.html'
+#
+#class RecipeAdmin (admin.ModelAdmin):
+# fieldsets = [
+# (None, {'fields': ['name']}),
+# ('Metadata', {'fields': ['author', 'source', 'url', 'tags'],
+# 'classes': ['collapse']}),
+# ('Yield', {'fields': ['unit', 'value', 'min_value', 'max_value'],
+# 'classes': ['collapse']}),
+# ('Directions', {'fields': ['directions_markdown']}),
+# ]
+# inlines = [IngredientBlockInline]
+#
+# list_display = ['name']
+#
+
+#_admin.site.register(_models.Recipe, RecipeAdmin)
+#_admin.site.register(_models.IngredientBlock, IngredientBlockAdmin)
+_admin.site.register(_models.NFPASpecial)
+_admin.site.register(_models.Chemical)
+_admin.site.register(_models.Location)
+_admin.site.register(_models.Vendor)
+_admin.site.register(_models.ChemicalInstance)
+++ /dev/null
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of ChemDB.
-#
-# ChemDB is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# ChemDB is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ChemDB. If not, see <http://www.gnu.org/licenses/>.
-
-"""Utilities for chemical inventories.
-
-Includes a CAS number validator and document generation.
-"""
-
-import re
-import os
-import os.path
-import time
-import types
-from sys import stdin, stdout, stderr
-
-from .db.text import DBPrettyPrinter
-
-
-def valid_CASno(cas_string, debug=False):
- """Validate CAS numbers.
-
- Check `N..NN-NN-N` format, and the `checksum digit`_ for valid CAS
- number structure. for
-
- .. math::
- N_n .. N_4 N_3 - N_2 N_1 - R
-
- The checksum digit is
-
- .. math::
- R = remainder([sum_{i=1}^n i N_i ] / 10 )
-
- .. _checksum digit:
- http://www.cas.org/expertise/cascontent/registry/checkdig.html
-
- >>> valid_CASno('107-07-3')
- True
- >>> valid_CASno('107-08-3')
- False
- >>> valid_CASno('107-083')
- False
-
- Sometimes we don't have a CAS number, or a product will contain
- secret, non-hazardous ingredients. Therefore we treat the strings
- `na` and `+secret-non-hazardous` as valid CAS numbers.
-
- >>> valid_CASno('na')
- True
- >>> valid_CASno('+secret-non-hazardous')
- True
- """
- if cas_string in ['na', '+secret-non-hazardous']:
- return True
- # check format,
- # \A matches the start of the string
- # \Z matches the end of the string
- regexp = re.compile('\A[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 = [int(d) for d in ''.join(cas_string.split('-'))]
- sumdigs = casdigs[:-1]
- sumdigs.reverse()
- s = sum([(i+1)*d for i,d in enumerate(sumdigs)])
- check = s % 10
- if check == casdigs[-1]:
- return True
- elif debug:
- print >> stderr, (
- "invalid CAS# check: '%s' (expected %d)" % (cas_string, check))
- return False
-
-class MSDSManager (object):
- """Manage Material Saftey Data Sheets (MSDSs).
- """
- def __init__(self, db, dir="./MSDS/"):
- self.db = db
- self.dir = dir
- self.MIMEs = {
- 'application/pdf': ['pdf'],
- 'text/html': ['html'],
- 'text/plain': ['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 isinstance(id, int), (
- '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 isinstance(id, int), (
- 'id must be an integer, not %s (%s)' % (type(id), str(id)))
- return "./%d" % id
-
- def MIME_ext(self, mime):
- if mime in self.MIMEs.keys():
- return self.MIMEs[mime][0]
- for values in self.MIMEs.values():
- if mime in values:
- return mime
- raise ValueError(
- "invalid MIME type '%s'\nshould be one of %s" % (mime, self.MIMEs))
-
- 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 = MSDSManager(db=None)
- >>> print m.has_MSDS_MIME(102, 'pdf') # test on html
- False
- >>> print m.has_MSDS_MIME(102, 'html') # test on html
- True
- >>> print m.has_MSDS_MIME(6, 'pdf') # test on pdf symlink
- True
- """
- return os.path.exists(self.path(id, mime))
-
- def get_MSDS_path(self, id):
- """
- >>> m = MSDSManager(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 = MSDSManager(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, doc_root=os.path.join('template', 'doc')):
- self.db = db
- self.doc_root = doc_root
-
- def _latex_safe(self, string):
- string = string.replace('%', '\%')
- string = string.replace('>', '$>$')
- string = string.replace('<', '$<$')
- return string
-
- def _set_main_target(self, target):
- print >> file(os.path.join(self.doc_root, 'main.tex'), 'w'), (
- """\documentclass[letterpaper]{article}
-
-\input{%s}
-""" % target)
-
- def _make_pdf(self, target_file):
- os.system('cd %s && make pdf' % self.doc_root)
- path = os.path.join(self.doc_root, target_file)
- os.system('cp %s %s' % (os.path.join(self.doc_root, 'main.pdf'), path))
- return path
-
- def inventory(self, title=None,
- namewidth='a', sort_field='db_id',
- valid_record=lambda r: r['Disposed'] == ''):
- """Create a pdf list of all maching chemicals. The default is to
- match all currently owned chemicals. Matching chemicals can be sorted
- by any field (defaults to 'ID')."""
- if title == None:
- title == 'Inventory'
- pp = DBPrettyPrinter(self.db)
- active_ids = []
- for record in self.db.records():
- if valid_record(record) : # get ids for matching chemicals
- active_ids.append(record['db_id'])
- active_ids.sort(cmp=lambda a,b: cmp(self.db.record(a)[sort_field],
- self.db.record(b)[sort_field]))
- 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(os.path.join(self.doc_root, 'inventory_title.tex'), 'w'), title
- print >> file(os.path.join(self.doc_root, '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,
- valid_record=lambda r: r['Disposed'] == ''):
- """create a warning NFPA diamond and list of the most dangerous
- chemicals for which valid_record(record) is true. For
- example, to generate a door warning for the front door use
- door_warning(lambda r: r['Disposed'] == '')
- or to generate the warning for the fridge
- door_warning(lambda r: r['Disposed'] == '' and r['Location'] == 'Refrigerator')
- Note that valid_record defaults to the first example.
- """
- # 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 valid_record(record):
- 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 valid_record(record):
- 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
- # setup the NFPA grapic
- oxstring = 'oxidizer' if 'OX' in NFPA_maxs['O'] else None
- wstring = 'nowater' if 'W' in NFPA_maxs['O'] else None
- extra_specials = [
- x for x in NFPA_maxs['O'] if x not in ['OX', 'W']]
- esstring = None
- if len(extra_specials) > 0:
- esstring = 'special={%s}' % (
- ','.join([x for x in extra_specials]))
- NFPA_maxs['special_args'] = ', '.join([
- x for x in [oxstring, wstring, esstring] if x != None])
- string = """
-\\begin{center}
- \\Huge
- \\firediamond{health=%(H)s, flammability=%(F)s, reactivity=%(R)s,
- %(special_args)s}
-\\end{center}
-""" % NFPA_maxs
- # now generate a list of the nasties ( Amount & ID & Name )
- string += """
-\\vspacer
-
-\\contfont
-
-\\begin{tabular}{r r l}
-"""
- for field,name,array in zip(['H', 'F', 'R', 'O'],
- ['Health', 'Fire',
- 'Reactivity', 'Other'],
- [Healths, Fires,
- Reactivities, Others]):
- if (not hasattr(NFPA_maxs[field], '__len__')) \
- or len(NFPA_maxs[field]) > 0:
- string += " \multicolumn{3}{c}{\Tstrut %s : %s} \\\\\n" \
- % (name, NFPA_maxs[field])
- else : # Print "Other" instead of "Other : []"
- string += " \multicolumn{3}{c}{\Tstrut %s} \\\\\n" \
- % (name)
- 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(os.path.join(self.doc_root, '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()
-
-if __name__ == "__main__":
- _test()
+++ /dev/null
-"""Daemon base class
-Shane Hathaway
-http://hathawaymix.org/Software/Sketches/daemon.py
-
-Provides a framework for daemonizing a process. Features:
-
- - reads the command line
-
- - reads a configuration file
-
- - configures logging
-
- - calls root-level setup code
-
- - drops privileges
-
- - calls user-level setup code
-
- - detaches from the controlling terminal
-
- - checks and writes a pidfile
-
-
-Example daemon:
-
-import daemon
-import logging
-import time
-
-class HelloDaemon(daemon.Daemon):
- default_conf = '/etc/hellodaemon.conf'
- section = 'hello'
-
- def run(self):
- while True:
- logging.info('The daemon says hello')
- time.sleep(1)
-
-if __name__ == '__main__':
- HelloDaemon().main()
-
-
-Example hellodaemon.conf:
-
-[hello]
-uid =
-gid =
-pidfile = ./hellodaemon.pid
-logfile = ./hellodaemon.log
-loglevel = info
-
-"""
-
-import ConfigParser
-import errno
-import grp
-import logging, logging.handlers
-import optparse
-import os
-import pwd
-import signal
-import sys
-import time
-
-
-class Daemon(object):
- """Daemon base class"""
-
- default_conf = '' # override this
- section = 'daemon' # override this
-
- def setup_root(self):
- """Override to perform setup tasks with root privileges.
-
- When this is called, logging has been initialized, but the
- terminal has not been detached and the pid of the long-running
- process is not yet known.
- """
-
- def setup_user(self):
- """Override to perform setup tasks with user privileges.
-
- Like setup_root, the terminal is still attached and the pid is
- temporary. However, the process has dropped root privileges.
- """
-
- def run(self):
- """Override.
-
- The terminal has been detached at this point.
- """
-
- def main(self):
- """Read the command line and either start or stop the daemon"""
- self.parse_options()
- action = self.options.action
- self.read_basic_config()
- if action == 'start':
- self.start()
- elif action == 'stop':
- self.stop()
- else:
- raise ValueError(action)
-
- def parse_options(self):
- """Parse the command line"""
- p = optparse.OptionParser()
- p.add_option('--start', dest='action',
- action='store_const', const='start', default='start',
- help='Start the daemon (the default action)')
- p.add_option('-s', '--stop', dest='action',
- action='store_const', const='stop', default='start',
- help='Stop the daemon')
- p.add_option('-c', dest='config_filename',
- action='store', default=self.default_conf,
- help='Specify alternate configuration file name')
- p.add_option('-n', '--nodaemon', dest='daemonize',
- action='store_false', default=True,
- help='Run in the foreground')
- self.options, self.args = p.parse_args()
- if not os.path.exists(self.options.config_filename):
- p.error('configuration file not found: %s'
- % self.options.config_filename)
-
- def read_basic_config(self):
- """Read basic options from the daemon config file"""
- self.config_filename = self.options.config_filename
- cp = ConfigParser.ConfigParser()
- cp.read([self.config_filename])
- self.config_parser = cp
-
- try:
- self.uid, self.gid = get_uid_gid(cp, self.section)
- except ValueError, e:
- sys.exit(str(e))
-
- self.pidfile = cp.get(self.section, 'pidfile')
- self.logfile = cp.get(self.section, 'logfile')
- self.loglevel = cp.get(self.section, 'loglevel')
-
- def on_sigterm(self, signalnum, frame):
- """Handle segterm by treating as a keyboard interrupt"""
- raise KeyboardInterrupt('SIGTERM')
-
- def add_signal_handlers(self):
- """Register the sigterm handler"""
- signal.signal(signal.SIGTERM, self.on_sigterm)
-
- def start(self):
- """Initialize and run the daemon"""
- # The order of the steps below is chosen carefully.
- # - don't proceed if another instance is already running.
- self.check_pid()
- # - start handling signals
- self.add_signal_handlers()
- # - create log file and pid file directories if they don't exist
- self.prepare_dirs()
-
- # - start_logging must come after check_pid so that two
- # processes don't write to the same log file, but before
- # setup_root so that work done with root privileges can be
- # logged.
- self.start_logging()
- try:
- # - set up with root privileges
- self.setup_root()
- # - drop privileges
- self.set_uid()
- # - check_pid_writable must come after set_uid in order to
- # detect whether the daemon user can write to the pidfile
- self.check_pid_writable()
- # - set up with user privileges before daemonizing, so that
- # startup failures can appear on the console
- self.setup_user()
-
- # - daemonize
- if self.options.daemonize:
- daemonize()
- except:
- logging.exception("failed to start due to an exception")
- raise
-
- # - write_pid must come after daemonizing since the pid of the
- # long running process is known only after daemonizing
- self.write_pid()
- try:
- logging.info("started")
- try:
- self.run()
- except (KeyboardInterrupt, SystemExit):
- pass
- except:
- logging.exception("stopping with an exception")
- raise
- finally:
- self.remove_pid()
- logging.info("stopped")
-
- def stop(self):
- """Stop the running process"""
- if self.pidfile and os.path.exists(self.pidfile):
- pid = int(open(self.pidfile).read())
- os.kill(pid, signal.SIGTERM)
- # wait for a moment to see if the process dies
- for n in range(10):
- time.sleep(0.25)
- try:
- # poll the process state
- os.kill(pid, 0)
- except OSError, why:
- if why[0] == errno.ESRCH:
- # process has died
- break
- else:
- raise
- else:
- sys.exit("pid %d did not die" % pid)
- else:
- sys.exit("not running")
-
- def prepare_dirs(self):
- """Ensure the log and pid file directories exist and are writable"""
- for fn in (self.pidfile, self.logfile):
- if not fn:
- continue
- parent = os.path.dirname(fn)
- if not os.path.exists(parent):
- os.makedirs(parent)
- self.chown(parent)
-
- def set_uid(self):
- """Drop root privileges"""
- if self.gid:
- try:
- os.setgid(self.gid)
- except OSError, (code, message):
- sys.exit("can't setgid(%d): %s, %s" %
- (self.gid, code, message))
- if self.uid:
- try:
- os.setuid(self.uid)
- except OSError, (code, message):
- sys.exit("can't setuid(%d): %s, %s" %
- (self.uid, code, message))
-
- def chown(self, fn):
- """Change the ownership of a file to match the daemon uid/gid"""
- if self.uid or self.gid:
- uid = self.uid
- if not uid:
- uid = os.stat(fn).st_uid
- gid = self.gid
- if not gid:
- gid = os.stat(fn).st_gid
- try:
- os.chown(fn, uid, gid)
- except OSError, (code, message):
- sys.exit("can't chown(%s, %d, %d): %s, %s" %
- (repr(fn), uid, gid, code, message))
-
- def start_logging(self):
- """Configure the logging module"""
- try:
- level = int(self.loglevel)
- except ValueError:
- level = int(logging.getLevelName(self.loglevel.upper()))
-
- handlers = []
- if self.logfile:
- handlers.append(logging.handlers.RotatingFileHandler( \
- self.logfile, maxBytes=10000, backupCount=5))
- self.chown(self.logfile)
- if not self.options.daemonize:
- # also log to stderr
- handlers.append(logging.StreamHandler())
-
- log = logging.getLogger()
- log.setLevel(level)
- for h in handlers:
- h.setFormatter(logging.Formatter(
- "%(asctime)s %(process)d %(levelname)s %(message)s"))
- log.addHandler(h)
-
- def check_pid(self):
- """Check the pid file.
-
- Stop using sys.exit() if another instance is already running.
- If the pid file exists but no other instance is running,
- delete the pid file.
- """
- if not self.pidfile:
- return
- # based on twisted/scripts/twistd.py
- if os.path.exists(self.pidfile):
- try:
- pid = int(open(self.pidfile).read().strip())
- except ValueError:
- msg = 'pidfile %s contains a non-integer value' % self.pidfile
- sys.exit(msg)
- try:
- os.kill(pid, 0)
- except OSError, (code, text):
- if code == errno.ESRCH:
- # The pid doesn't exist, so remove the stale pidfile.
- os.remove(self.pidfile)
- else:
- msg = ("failed to check status of process %s "
- "from pidfile %s: %s" % (pid, self.pidfile, text))
- sys.exit(msg)
- else:
- msg = ('another instance seems to be running (pid %s), '
- 'exiting' % pid)
- sys.exit(msg)
-
- def check_pid_writable(self):
- """Verify the user has access to write to the pid file.
-
- Note that the eventual process ID isn't known until after
- daemonize(), so it's not possible to write the PID here.
- """
- if not self.pidfile:
- return
- if os.path.exists(self.pidfile):
- check = self.pidfile
- else:
- check = os.path.dirname(self.pidfile)
- if not os.access(check, os.W_OK):
- msg = 'unable to write to pidfile %s' % self.pidfile
- sys.exit(msg)
-
- def write_pid(self):
- """Write to the pid file"""
- if self.pidfile:
- open(self.pidfile, 'wb').write(str(os.getpid()))
-
- def remove_pid(self):
- """Delete the pid file"""
- if self.pidfile and os.path.exists(self.pidfile):
- os.remove(self.pidfile)
-
-
-def get_uid_gid(cp, section):
- """Get a numeric uid/gid from a configuration file.
-
- May return an empty uid and gid.
- """
- uid = cp.get(section, 'uid')
- if uid:
- try:
- int(uid)
- except ValueError:
- # convert user name to uid
- try:
- uid = pwd.getpwnam(uid)[2]
- except KeyError:
- raise ValueError("user is not in password database: %s" % uid)
-
- gid = cp.get(section, 'gid')
- if gid:
- try:
- int(gid)
- except ValueError:
- # convert group name to gid
- try:
- gid = grp.getgrnam(gid)[2]
- except KeyError:
- raise ValueError("group is not in group database: %s" % gid)
-
- return uid, gid
-
-class PrintLogger(object):
- '''
- This class by Peter Parente, for the Jambu project
- http://www.oatsoft.org/trac/jambu/browser/trunk/JambuLog.py?rev=1
-
- @author: Peter Parente
- @organization: IBM Corporation
- @copyright: Copyright (c) 2005, 2007 IBM Corporation
- @license: The BSD License
-
- All rights reserved. This program and the accompanying materials are made
- available under the terms of the BSD license which accompanies
- this distribution, and is available at
- U{http://www.opensource.org/licenses/bsd-license.php}
-
- Provides a dirt-simple interface compatible with stdout and stderr. When
- assigned to sys.stdout or sys.stderr, an instance of this class redirects
- print statements to the logging system. This means the result of the
- print statements can be silenced, sent to a file, etc. using the command
- line options to LSR. The log level used is defined by the L{LEVEL} constant
- in this class.
-
- @cvar LEVEL: Logging level for writes directed through this class, above
- DEBUG and below INFO
- @type LEVEL: integer
- @ivar log: Reference to the Print log channel
- @type log: logging.Logger
-
- With minor adjustments by WTK
- '''
- LEVEL = 20
-
- def __init__(self, logger=None):
- '''
- Create the logger.
- '''
- if logger == None:
- self.log = logging.getLogger('print')
- else:
- self.log = logger
- self.chunks = []
- self.flush = None
-
- def write(self, data):
- '''
- Write the given data at the debug level to the logger. Stores chunks of
- text until a new line is encountered, then sends to the logger.
-
- @param data: Any object that can be converted to a string
- @type data: stringable
- '''
- s = data.encode('utf-8')
- if s.endswith('\n'):
- self.chunks.append(s[:-1])
- s = ''.join(self.chunks)
- self.log.log(self.LEVEL, s)
- self.chunks = []
- else:
- self.chunks.append(s)
-
-def daemonize():
- """Detach from the terminal and continue as a daemon.
- Added support for redirecting stdout & stderr to logs.
- See
- http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
- WTK 2008."""
- # swiped from twisted/scripts/twistd.py
- # See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16
- if os.fork(): # launch child and...
- os._exit(0) # kill off parent
- os.setsid()
- # some people os.chdir('/') here so they don't hog the start directory.
- if os.fork(): # launch child and...
- os._exit(0) # kill off parent again.
- os.umask(077)
- # flush any pending output
- #sys.stdin.flush()?
- sys.stdout.flush()
- sys.stderr.flush()
-
- # hook up to /dev/null
- null=os.open('/dev/null', os.O_RDWR)
- for i in range(3):
- try:
- os.dup2(null, i)
- except OSError, e:
- if e.errno != errno.EBADF:
- raise
- os.close(null)
- # I'd like to hook up to the logfiles instead, but they don't have filenos...
- #os.dup2(out_log.fileno(), sys.stdout.fileno())
- #os.dup2(err_log.fileno(), sys.stderr.fileno())
- #os.dup2(dev_null.fileno(), sys.stdin.fileno())
-
- # I can hook up sys.stdin and sys.stderr to the logfiles
- log = logging.getLogger()
- #sys.stdout = PrintLogger(log) # let web.py's stdout through...
- sys.stderr = PrintLogger(log)
+++ /dev/null
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of ChemDB.
-#
-# ChemDB is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# ChemDB is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ChemDB. If not, see <http://www.gnu.org/licenses/>.
+++ /dev/null
-# Copyright (C) 2008-2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of ChemDB.
-#
-# ChemDB is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# ChemDB is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ChemDB. If not, see <http://www.gnu.org/licenses/>.
-
-"""Database-style interface with a text-delimited, single files.
-
-Use this if, for example, your coworkers insist on keeping data
-compatible with M$ Excel.
-"""
-
-import copy
-import os
-import os.path
-import shutil
-import stat
-from sys import stdin, stdout, stderr
-import time
-import types
-
-
-FILE = 'default.db'
-STANDARD_TAB = 8
-
-
-class IndexStringError (Exception):
- "invalid index string format"
- pass
-
-
-class FieldError (Exception):
- "database fields are not unique"
- pass
-
-
-class MissingDatabaseFile (Exception):
- "Specified database file does not exist"
- def __init__(self, path):
- msg = "Missing database file %s" % path
- Exception.__init__(self, msg)
- self.path = path
-
-
-class TextDB (object):
- """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"
- 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.
- """
- if not self.exists():
- raise MissingDatabaseFile(self.curpath())
-
- def _open(self):
- "Load the database from disk"
- self._assert_exists()
- 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."
- 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
- tname = '%s.%d' % (self.filename, 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 = TextDB(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 = TextDB(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.pop(0), assert_unique=True)
- self._long_header = None
- self._records = []
- if len(reclines) == 0:
- return # Only a header
- # check for a long-header line
- if len(reclines[0]) > 0 and reclines[0].startswith(self.COM_CHAR):
- self._long_header = self._get_header(reclines.pop(0))
- for recline in reclines:
- if len(recline) == 0 or recline.startswith(self.COM_CHAR):
- continue # ignore blank lines and comments
- 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 = TextDB(FS=':', RS=';', filename=None)
-
- >>> rs = '2-Propanol:4 L:67-63-0:Fisher:6/6/2004:2:3:0'
- >>> db._file_record_string(db._get_fields(rs))
- '2-Propanol:4 L:67-63-0:Fisher:6/6/2004:2:3:0;'
- """
- 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 a 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 isinstance(db_id, int), "id %s not an int!" % str(db_id)
- assert db_id >= 0 and 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 with data from the field-name-keyed dict `newvals`.
-
- Overwrites any preexisting data.
- """
- 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 DBPrettyPrinter (object):
- """Define some pretty-print functions for :class:`TextDB` objects.
- """
- def __init__(self, db):
- self.db = db
-
- def _norm_active_fields(self, active_fields_in=None):
- """Normalize the active field parameter
-
- >>> from pprint import pprint
- >>> db = TextDB(FS=':', RS=' ; ', filename=None)
- >>> pp = DBPrettyPrinter(db)
- >>> db._parse("#Name:Amount:CAS#:Vendor ; 2-Propanol:4 L:67-63-0:Fisher")
-
- >>> pprint(pp._norm_active_fields(None))
- {'Amount': True, 'CAS#': True, 'Name': True, 'Vendor': True}
- >>> pprint(pp._norm_active_fields(['Vendor', 'Amount']))
- {'Amount': True, 'CAS#': False, 'Name': False, 'Vendor': True}
- >>> pprint(pp._norm_active_fields('1:3'))
- {'Amount': True, 'CAS#': True, 'Name': False, 'Vendor': False}
- """
- if active_fields_in == None:
- active_fields = {}
- for field in self.db.field_list():
- active_fields[field] = True
- return active_fields
- elif isinstance(active_fields_in, types.StringTypes):
- 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 isinstance(active_fields_in, list):
- active_fields = {}
- for field in active_fields_in:
- active_fields[field] = True
- elif isinstance(active_fields_in, dict):
- active_fields = active_fields_in
- assert isinstance(active_fields, dict), '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 = TextDB(FS=':', RS=' ; ', filename=None)
- >>> pp = DBPrettyPrinter(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 = DBPrettyPrinter('dummy')
- >>> pp._istr2ilist('0,2,89,4')
- [0, 2, 89, 4]
- >>> pp._istr2ilist('1:6')
- [1, 2, 3, 4, 5]
- >>> 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 isinstance(width_in, int) 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 isinstance(record_ids, types.StringTypes):
- 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 = TextDB(FS=':', RS=' ; ', filename=None)
-
- >>> pp = DBPrettyPrinter(db)
- >>> db._parse("#Name:Amount:CAS#:Vendor ; 2-Propanol:4 L:67-63-0:Fisher")
- >>> 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 = TextDB(FS=':', RS=' ; ', filename=None)
-
- >>> pp = DBPrettyPrinter(db)
- >>> db._parse("#Name:Amount:CAS#:Vendor ; 2-Propanol:4 L:67-63-0:Fisher")
- >>> 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()
--- /dev/null
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# This file is part of ChemDB.
+#
+# ChemDB is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# ChemDB is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ChemDB. If not, see <http://www.gnu.org/licenses/>.
+
+"""Generate inventories and other required documentation.
+"""
+
+import collections as _collections
+import os as _os
+import os.path as _os_path
+import shutil as _shutil
+import subprocess as _subprocess
+import tempfile as _tempfile
+import time as _time
+import types as _types
+
+from django.template import loader as _loader
+
+from . import LOG as _LOG
+from .templatetags import latex as _latex
+
+
+class DocGen (object):
+ "Generate the officially required documents"
+ def __init__(self, chemical_instances):
+ self.chemical_instances = chemical_instances
+
+ def _make_pdf(self, files):
+ assert 'main.tex' in files, files.keys()
+ tmp_dir = None
+ try:
+ tmp_dir = _tempfile.mkdtemp()
+ for filename,content in files.items():
+ with open(_os_path.join(tmp_dir, filename), 'wb') as f:
+ f.write(content)
+ status,stdout,stderr = self._invoke_pdflatex(cwd=tmp_dir)
+ for i in range(3):
+ if 'Rerun to get cross-references right' not in stdout:
+ break
+ status,stdout,stderr = self._invoke_pdflatex(cwd=tmp_dir)
+ pdf = open(_os_path.join(tmp_dir, 'main.pdf'), 'rb').read()
+ finally:
+ if tmp_dir:
+ _shutil.rmtree(tmp_dir)
+ return pdf
+
+ def _invoke_pdflatex(self, **kwargs):
+ p = _subprocess.Popen(
+ ['pdflatex', '-interaction=nonstopmode', 'main.tex'],
+ stdin=_subprocess.PIPE, stdout=_subprocess.PIPE,
+ stderr=_subprocess.PIPE, shell=False, close_fds=True, **kwargs)
+ stdout,stderr = p.communicate()
+ status = p.wait()
+ if status:
+ raise RuntimeError((status, stdout, stderr))
+ return (status, stdout, stderr)
+
+ def inventory(self, title=None):
+ """Create a pdf list of all chemicals.
+ """
+ if title == None:
+ title == 'Inventory'
+ tex = _loader.render_to_string(
+ 'chemdb/doc/inventory.tex', {
+ 'title': title,
+ 'chemical_instances': self.chemical_instances,
+ })
+ nfpa_704 = _loader.render_to_string('chemdb/doc/nfpa_704.sty')
+ return self._make_pdf({'main.tex': tex, 'nfpa_704.sty': nfpa_704})
+
+ def door_warning(self):
+ """Create an NFPA diamond and list of the most dangerous chemicals.
+ """
+ nfpa_max = {'health':0, 'fire':0, 'reactivity':0, 'special':set()}
+ nasties = _collections.defaultdict(list)
+ nasties['special'] = set()
+ for chemical_instance in self.chemical_instances:
+ for attr in ['health', 'fire', 'reactivity', 'special',
+ 'mutagen', 'carcinogen', 'teratogen']:
+ value = getattr(chemical_instance.chemical, attr)
+ if not value:
+ continue
+ if attr in ['health', 'fire', 'reactivity']:
+ if value > nfpa_max[attr]:
+ nfpa_max[attr] = value
+ nasties[attr] = [chemical_instance]
+ elif value == nfpa_max[attr]:
+ nasties[attr].append(chemical_instance)
+ elif attr == 'special':
+ if value.count():
+ nfpa_max[attr].update(value.all())
+ nasties[attr].add(chemical_instance)
+ else:
+ nasties[attr].append(chemical_instance)
+
+ instance_groups = [
+ ('Health: {}'.format(nfpa_max['health']), nasties['health']),
+ ('Fire: {}'.format(nfpa_max['fire']), nasties['fire']),
+ ('Reactivity: {}'.format(nfpa_max['reactivity']),
+ nasties['reactivity']),
+ ('Special: {}'.format(_latex.latex_specials(nfpa_max['special'])),
+ nasties['special']),
+ ('Mutagen', nasties['mutagen']),
+ ('Carcinogen', nasties['carcinogen']),
+ ('Teratogen', nasties['teratogen']),
+ ]
+ tex = _loader.render_to_string(
+ 'chemdb/doc/door.tex', {
+ 'health': nfpa_max['health'],
+ 'fire': nfpa_max['fire'],
+ 'reactivity': nfpa_max['reactivity'],
+ 'special': nfpa_max['special'],
+ 'instance_groups': instance_groups,
+ })
+
+ nfpa_704 = _loader.render_to_string('chemdb/doc/nfpa_704.sty')
+ return self._make_pdf({'main.tex': tex, 'nfpa_704.sty': nfpa_704})
--- /dev/null
+- model: chemdb.CASNumber
+ pk: 1
+ fields:
+ name: Acetic Acid (CH₃COOH)
+ abbrev: Acetic Acid
+ cas: 64-19-7
+- model: chemdb.CASNumber
+ pk: 2
+ fields:
+ name: Ammonium Persulfate ((NH₄)₂S₂O₈)
+ abbrev: Ammonium Persulfate
+ cas: 7727-54-0
+- model: chemdb.CASNumber
+ pk: 3
+ fields:
+ name: Glycerin (C₃H₈O₃)
+ abbrev: Glycerin
+ cas: 56-81-5
+- model: chemdb.CASNumber
+ pk: 4
+ fields:
+ name: Hydrogen Chloride (HCl)
+ abbrev: Hydrogen Chloride
+ cas: 7647-01-0
+- model: chemdb.CASNumber
+ pk: 5
+ fields:
+ name: Water (H₂O)
+ abbrev: Water
+ cas: 7732-18-5
+- model: chemdb.CASNumber
+ pk: 6
+ fields:
+ name: Glycine (NH₂CH₂COOH)
+ abbrev: Glycine
+ cas: 56-40-6
+- model: chemdb.CASNumber
+ pk: 7
+ fields:
+ name: Phenol (C₆H₅OH)
+ abbrev: Phenol
+ cas: 108-95-2
+- model: chemdb.CASNumber
+ pk: 8
+ fields:
+ name: Sodium Chloride (NaCl)
+ abbrev: Sodium Chloride
+ cas: 7647-14-5
+- model: chemdb.CASNumber
+ pk: 9
+ fields:
+ name: Sodium Hydroxide (NaOH)
+ abbrev: Sodium Hydroxide
+ cas: 1310-73-2
+
+- model: chemdb.Chemical
+ pk: 1
+ fields:
+ name: Acetic Acid (CH₃COOH)
+ abbrev: Acetic Acid
+ cas: [1]
+ msds: msds/Acetic_Acid.pdf
+ health: 3
+ fire: 2
+ reactivity: 1
+ mutagen: no
+ carcinogen: no
+ teratogen: no
+- model: chemdb.Chemical
+ pk: 2
+ fields:
+ name: Ammonium Persulfate ((NH₄)₂S₂O₈)
+ abbrev: Ammonium Persulfate
+ cas: [2]
+ health: 3
+ fire: 0
+ reactivity: 3
+ special: [1]
+ mutagen: no
+ carcinogen: no
+ teratogen: no
+- model: chemdb.Chemical
+ pk: 3
+ fields:
+ name: Glycerin Solution 50% (C₃H₅(OH)₃)
+ abbrev: 50% Glycerin
+ cas: [3, 4, 5]
+ health: 1
+ fire: 0
+ reactivity: 1
+ special: [4]
+ mutagen: yes
+ carcinogen: no
+ teratogen: yes
+ note: Labled H2 F0 R0 in NFPA diamond on the bottle. Avoid strong bases and oxidizers.
+- model: chemdb.Chemical
+ pk: 4
+ fields:
+ name: Glycine (NH₂CH₂COOH)
+ abbrev: Glycine
+ cas: [6]
+ health: 1
+ fire: 0
+ reactivity: 0
+ special: [4]
+ mutagen: no
+ carcinogen: no
+ teratogen: no
+ note: Avoid strong oxidizers.
+- model: chemdb.Chemical
+ pk: 5
+ fields:
+ name: Phenol (C₆H₅OH)
+ abbrev: Phenol
+ cas: [7]
+ health: 3
+ fire: 2
+ reactivity: 0
+ special: [4]
+ mutagen: yes
+ carcinogen: yes
+ teratogen: yes
+ note: Avoid strong oxidizers (especially calcium hypochlorite), acids, and halogens.
+- model: chemdb.Chemical
+ pk: 6
+ fields:
+ name: Sodium Chloride (NaCl)
+ abbrev: Sodium Chloride
+ cas: [8]
+ health: 1
+ fire: 0
+ reactivity: 0
+ mutagen: no
+ carcinogen: no
+ teratogen: no
+- model: chemdb.Chemical
+ pk: 7
+ fields:
+ name: Sodium Hydroxide (NaOH) 50% (w/w) solution
+ abbrev: 50% Sodium Hydroxide
+ cas: [9, 5]
+ health: 3
+ fire: 0
+ reactivity: 2
+ special: [2]
+ mutagen: no
+ carcinogen: no
+ teratogen: no
+
+- model: chemdb.Location
+ pk: 1
+ fields:
+ name: acidic liquids
+ abbrev: acidic liquids
+- model: chemdb.Location
+ pk: 2
+ fields:
+ name: basic liquids
+ abbrev: basic liquids
+- model: chemdb.Location
+ pk: 3
+ fields:
+ name: corrosive liquids
+ abbrev: corrosive liquid
+- model: chemdb.Location
+ pk: 4
+ fields:
+ name: non-hazardous
+ abbrev: non-hazardous
+- model: chemdb.Location
+ pk: 5
+ fields:
+ name: oxidizing solids
+ abbrev: oxidizing solids
+- model: chemdb.Location
+ pk: 6
+ fields:
+ name: reactive solids
+ abbrev: reactive solids
+
+- model: chemdb.Vendor
+ pk: 1
+ fields:
+ name: 'Fisher Scientific International Inc. (NYSE:FSH)'
+ abbrev: Fisher
+ url: http://www.fishersci.com/
+- model: chemdb.Vendor
+ pk: 2
+ fields:
+ name: 'Life Technologies Coperation (NASDAQ:LIFE)'
+ abbrev: Life Tech.
+ url: http://www.lifetechnologies.com/
+- model: chemdb.Vendor
+ pk: 3
+ fields:
+ name: 'Sigma-Aldrich Corperation (NASDAQ:SIAL)'
+ abbrev: Sigma
+ url: http://www.sigmaaldrich.com/
+- model: chemdb.Vendor
+ pk: 4
+ fields:
+ name: VWR International, LLC
+ abbrev: VWR
+ url: https://www.vwr.com/
+ note: VWR International, LLC is a principle operating subsidiary of VWR Funding, which in turn is a portfolio company of private equity firm Madison Dearborn Partners.
+
+- model: chemdb.ChemicalInstance
+ pk: 1
+ fields:
+ chemical: 1
+ location: 1
+ amount: 500 mL
+ vendor: 1
+ catalog: A35-500
+ recieved: 2001-07-05
+- model: chemdb.ChemicalInstance
+ pk: 2
+ fields:
+ chemical: 2
+ location: 5
+ amount: 100 g
+ vendor: 2
+ catalog: 15523-012
+ recieved: 2000-01-16
+- model: chemdb.ChemicalInstance
+ pk: 3
+ fields:
+ chemical: 3
+ location: 3
+ amount: 500 mL
+ vendor: 4
+ catalog: VW3410-2
+ recieved: 2000-01-17
+- model: chemdb.ChemicalInstance
+ pk: 4
+ fields:
+ chemical: 4
+ location: 3
+ amount: 500 g
+ vendor: 2
+ catalog: 15527-013
+ recieved: 2000-01-16
+- model: chemdb.ChemicalInstance
+ pk: 5
+ fields:
+ chemical: 5
+ location: 6
+ amount: 100 g
+ vendor: 3
+ catalog: P1037
+ recieved: 2006-01-17
+- model: chemdb.ChemicalInstance
+ pk: 6
+ fields:
+ chemical: 6
+ location: 4
+ amount: 500 g
+ vendor: 4
+ catalog: JT4058-1
+ recieved: 2000-10-22
+- model: chemdb.ChemicalInstance
+ pk: 7
+ fields:
+ chemical: 7
+ location: 2
+ amount: 500 mL
+ vendor: 4
+ catalog: JT3727-1
+ recieved: 2000-10-22
--- /dev/null
+- model: chemdb.NFPASpecial
+ pk: 1
+ fields:
+ name: oxidizer
+ abbrev: OX
+- model: chemdb.NFPASpecial
+ pk: 2
+ fields:
+ name: water reactive
+ abbrev: -W-
+ symbol: W̶
+- model: chemdb.NFPASpecial
+ pk: 3
+ fields:
+ name: asphixiant
+ abbrev: AS
+- model: chemdb.NFPASpecial
+ pk: 4
+ fields:
+ name: corrosive
+ abbrev: COR
+- model: chemdb.NFPASpecial
+ pk: 5
+ fields:
+ name: strong acid
+ abbrev: ACID
+- model: chemdb.NFPASpecial
+ pk: 6
+ fields:
+ name: strong base
+ abbrev: ALK
+- model: chemdb.NFPASpecial
+ pk: 7
+ fields:
+ name: biological hazard
+ abbrev: BIO
+ symbol: ☣
+- model: chemdb.NFPASpecial
+ pk: 8
+ fields:
+ name: cryogenic
+ abbrev: CRY
+ symbol: ❄
+- model: chemdb.NFPASpecial
+ pk: 9
+ fields:
+ name: poisonous
+ abbrev: POI
+ symbol: ☠
+- model: chemdb.NFPASpecial
+ pk: 10
+ fields:
+ name: radioactive
+ abbrev: RAD
+ symbol: ☢
--- /dev/null
+# Copyright
+
+from django import forms as _forms
+
+from . import models as _models
+
+
+class LocationsForm (_forms.Form):
+ def __init__(self, *args, **kwargs):
+ choices = [(x.id, x.name) for x in _models.Location.objects.all()]
+ super(LocationsForm, self).__init__(*args, **kwargs)
+ self.fields['location'] = _forms.MultipleChoiceField(
+ widget=_forms.CheckboxSelectMultiple,
+ choices=choices, label='Locations')
--- /dev/null
+# Copyright
+
+from django.db import models as _models
+from django.forms import ValidationError as _ValidationError
+
+from . import LOG as LOG
+from . import util as _util
+
+
+class NamedItem (_models.Model):
+ name = _models.CharField(max_length=100)
+ abbrev = _models.CharField('abbreviation', max_length=20)
+
+ class Meta:
+ abstract = True
+ ordering = ['name']
+
+ def __unicode__(self):
+ return u'{0.abbrev}'.format(self)
+
+
+class NFPASpecial (NamedItem):
+ """An NFPA Special rating (e.g. 'OX', '-W-', 'SA', ...).
+ """
+ symbol = _models.CharField(max_length=3, blank=True, null=True)
+
+ def __unicode__(self):
+ if self.symbol:
+ return u'{0.symbol}'.format(self)
+ return super(NFPASpecial, self).__unicode__()
+
+
+class CASNumber (NamedItem):
+ "Chemical Abstracts Service registery number"
+ cas = _models.CharField(
+ 'CAS#', max_length=20)
+
+ def clean(self):
+ if not _util.valid_CASno(self.cas):
+ raise _ValidationError("invalid CAS number '{}'".format(self.cas))
+
+
+class Chemical (NamedItem):
+ """A chemical (in the abstract, not an instance of the chemical)
+
+ Separating ``Chemical``\s from ``ChemicalInstance``\s avoids
+ duplicate information (e.g. you can have two bottles of acetic
+ acid).
+ """
+ cas = _models.ManyToManyField(CASNumber, blank=True, null=True)
+ msds = _models.FileField(
+ 'Material safety data sheet', upload_to=_util.chemical_upload_to,
+ blank=True, null=True)
+ health = _models.PositiveIntegerField('NFPA health rating')
+ fire = _models.PositiveIntegerField('NFPA fire rating')
+ reactivity = _models.PositiveIntegerField('NFPA reactivity rating')
+ special = _models.ManyToManyField(NFPASpecial, blank=True, null=True)
+ mutagen = _models.NullBooleanField()
+ carcinogen = _models.NullBooleanField()
+ teratogen = _models.NullBooleanField()
+ note = _models.TextField('notes', blank=True, null=True)
+
+ def cas_numbers(self):
+ if self.cas.count() == 0:
+ return 'unknown'
+ return ', '.join(cas.cas for cas in self.cas.all())
+
+ def specials(self):
+ return ' '.join(str(special) for special in self.special.all())
+
+
+class Location (NamedItem):
+ "A chemical storage location (e.g. 'acidic liquids')"
+ pass
+
+
+class Vendor (NamedItem):
+ "A chemical supplier"
+ url = _models.URLField('URL', blank=True, null=True)
+ note = _models.TextField('notes', blank=True, null=True)
+
+
+class ChemicalInstance (_models.Model):
+ """An instance of a ``Chemical``
+
+ For example, 1L of acetic acid from Vendor X.
+ """
+ chemical = _models.ForeignKey(Chemical, related_name='chemical_instances')
+ location = _models.ForeignKey(Location, related_name='chemical_instances')
+ amount = _models.CharField(max_length=100)
+ vendor = _models.ForeignKey(Vendor, related_name='chemical_instances')
+ catalog = _models.CharField('vendor catalog number', max_length=100)
+ recieved = _models.DateField(auto_now_add=True, editable=True)
+ disposed = _models.DateField(blank=True, null=True)
+
+ class Meta:
+ ordering = ['chemical', 'recieved', 'disposed', 'id']
+++ /dev/null
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of ChemDB.
-#
-# ChemDB is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# ChemDB is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ChemDB. If not, see <http://www.gnu.org/licenses/>.
-
-"""Chemical inventory web interface.
-"""
-
-from __future__ import with_statement
-
-from cgi import escape
-import os
-import os.path
-import re
-import sys, logging
-
-import cherrypy
-from jinja2 import Environment, FileSystemLoader
-
-from .daemon import Daemon
-from .db.text import TextDB
-from .chemdb import valid_CASno, MSDSManager, DocGen
-
-
-__version__ = "0.4"
-
-
-class Server (object):
- """ChemDB web interface."""
- def __init__(self, db=None, MSDS_manager=None, docs=None,
- template_root='template', static_dir='static',
- static_url='static'):
- self.db = db
- self.MSDS_manager = MSDS_manager
- self.docs = docs
- self.static_dir = static_dir
- self.static_url = static_url
- self.env = Environment(loader=FileSystemLoader(template_root))
- self.width_max = {'CAS#':12}
- self.entry_width = 50 # display width of entry fields
- self.raw_fields = ['ID','Name']
- self.MSDS_content_types = [
- 'text/plain', 'text/html', 'application/pdf']
-
- @cherrypy.expose
- def index(self, id=None):
- """Chemical index page.
- """
- self.db._refresh()
- if id == None:
- return self._index_page()
- else:
- return self._view_page(id=id)
-
- def _index_page(self):
- records = self.db.records()
- for record in records:
- for field in self.db.field_list():
- rstring = record[field]
- if field in self.width_max: # truncate if width is regulated...
- w = self.width_max[field]
- if len(rstring) > w: # ... and full string is too long.
- rstring = "%s..." % rstring[:w-3]
- # if the field isn't raw, protect special chars
- if not field in self.raw_fields:
- rstring = escape(rstring)
- if self.MSDS_manager.has_MSDS(record['db_id']):
- # link the id to the MSDS
- record['Name'] = (
- '<a href="%s">%s</a>'
- % (self._MSDS_path(record['db_id']), record['Name']))
- template = self.env.get_template('index.html')
- return template.render(fields=self.db.field_list(), records=records)
-
- def _view_page(self, id):
- db_id = int(id)
- record = self.db.record(db_id)
- MSDS_link = None
- if self.MSDS_manager.has_MSDS(db_id):
- path = self._MSDS_path(db_id)
- MSDS_link = '<a href="%s">%s</a>' % (
- path, os.path.basename(path))
- template = self.env.get_template('record.html')
- return template.render(fields=self.db.field_list(),
- long_fields=self.db.long_fields(),
- record=record,
- MSDS=MSDS_link,
- escape=escape)
-
- def _MSDS_path(self, db_id):
- path = self.MSDS_manager.get_MSDS_path(db_id)
- path = os.path.relpath(path, self.static_dir)
- path = os.path.join(self.static_url, path)
- return path
-
- @cherrypy.expose
- def edit(self, id=None, **kwargs):
- """Render the edit-record.html template for the specified record.
- """
- self.db._refresh()
- if kwargs:
- self._update_record(id=id, **kwargs)
- raise cherrypy.HTTPRedirect(u'.?id=%s' % id, status=303)
- MSDSs = self.MSDS_manager.get_all(simlinks=False)
- if id in [None, '-1']:
- record = dict([(field,'') for field in self.db.field_list()])
- record['db_id'] = '-1'
- record['ID'] = str(record['db_id'])
- else:
- db_id = int(id)
- record = self.db.record(db_id)
- if self.MSDS_manager.has_MSDS(db_id):
- MSDSs = None # only ask for an MSDS if we still need one
- template = self.env.get_template('edit-record.html')
- return template.render(fields=self.db.field_list(),
- long_fields=self.db.long_fields(),
- record=record,
- MSDSs=MSDSs,
- entry_width=self.entry_width,
- escape=escape)
-
- def _update_record(self, id=None, **kwargs):
- """Update a record with form input.
- """
- self.db._refresh()
- if id in [None, '-1']:
- record = self.db.new_record()
- logging.info('new record %s' % record['db_id'])
- else:
- db_id = int(id)
- record = self.db.record(db_id)
- update = False
- for field in self.db.field_list():
- if kwargs[field] != record[field]:
- # TODO: add validation!
- update = True
- record[field] = kwargs[field]
- if kwargs.get('MSDS source', None) == 'upload':
- # Handle any MSDS file actions
- f = kwargs['MSDS upload']
- contents = f.file.read()
- if len(contents) > 0 and f.type in self.MSDS_content_types:
- self.MSDS_manager.save(int(record['ID']), contents, f.type)
- elif kwargs.get('MSDS source', None) == 'share':
- logging.info('linking MSDS %d to %d'
- % (int(record['ID']),
- int(kwargs['MSDS share'])) )
- self.MSDS_manager.link(int(record['ID']),
- int(kwargs['MSDS share']))
- if update:
- self.db.set_record(record['db_id'], record, backup=True)
-
-
-class Docs (object):
- "Generate and serve assorted official documents."
- def __init__(self, db=None, docgen=None, template_root='template'):
- self.db = db
- self.docgen = docgen
- self.namewidth = 40
- self.env = Environment(loader=FileSystemLoader(template_root))
-
- @cherrypy.expose
- def index(self):
- """List the available documents.
- """
- template = self.env.get_template('docs.html')
- return template.render()
-
- @cherrypy.expose
- def inventory_pdf(self):
- self.db._refresh()
- path = self.docgen.inventory(namewidth=self.namewidth)
- cherrypy.response.headers['Content-Type'] = 'application/pdf'
- return file(path, 'rb').read()
-
- @cherrypy.expose
- def door_warning_pdf(self, location=None):
- self.db._refresh()
- if location == None:
- valid = lambda r: r['Disposed'] == ''
- else:
- regexp = re.compile(location, re.I) # Case insensitive
- valid = lambda r: r['Disposed'] == '' and regexp.match(r['Location'])
- path = self.docgen.door_warning(valid_record=valid)
- cherrypy.response.headers['Content-Type'] = 'application/pdf'
- return file(path, 'rb').read()
-
-
-class ServerDaemon (Daemon):
- def __init__(self):
- super(ServerDaemon, self).__init__()
- self.gid = None
- self.uid = None
- self.pidfile = './chem_web.pid'
- self.logfile = './chem_web.log'
- self.loglevel = 'INFO'
-
- def run(self):
- if cherrypy.__version__.startswith('3.'):
- cherrypy.quickstart(root=self.server, config=self.app_config)
-
-# if self.pkey_file == None or self.cert_file == None:
-# logging.info("http://%s:%d/" % web.validip(self.ip_address))
-# else:
-# logging.info("https://%s:%d/" % web.validip(self.ip_address))
-#
-# webpy_func = web.webpyfunc(urls, globals(), False)
-# wsgi_func = web.wsgifunc(webpy_func)
-# web.httpserver.runsimple(wsgi_func,
-# web.validip(self.ip_address),
-# 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__
- p = OptionParser(usage=usage_string, version=version_string)
-
- # Non-server options
- p.add_option('-t', '--test', dest='test', default=False,
- action='store_true', help='Run internal tests and exit.')
-
- # Daemon options
- p.add_option('--start', dest='action',
- action='store_const', const='start', default='start',
- help='Start the daemon (the default action).')
- p.add_option('--stop', dest='action',
- action='store_const', const='stop', default='start',
- help='Stop the daemon.')
- p.add_option('-n', '--nodaemon', dest='daemonize',
- action='store_false', default=True,
- help='Run in the foreground.')
-
- # Server options
- p.add_option('-a', '--address', dest='address', default='127.0.0.1',
- metavar='ADDR',
- help='Address that the server will bind to (%default).')
- p.add_option('-p', '--port', dest='port', default=8080, type='int',
- metavar='PORT',
- help='Port that the server will listen on (%default).')
- p.add_option('-s', '--secure', dest="secure",
- help="Run in secure (HTTPS) mode.",
- type='string', metavar="PKEY_FILE:CERT_FILE")
- p.add_option('-v', '--verbose', dest="verbose", action="store_true",
- help="Print lots of debugging information.",
- default=False)
- p.add_option('--static', dest='static', metavar='PATH',
- help="Path to the directory of static files (%default).",
- default=os.path.join('example', 'static'))
- p.add_option('--template', dest='template', metavar='PATH',
- help="Path to the directory of template files (%default).",
- default=os.path.join('template', 'web'))
- p.add_option('--doc', dest='doc', metavar='PATH',
- help="Path to the directory of document generation files (%default).",
- default=os.path.join('template', 'doc'))
- p.add_option('--htaccess', dest='htaccess', metavar='FILE',
- help="Path to the htaccess file (%default).",
- default='.htaccess')
-
- # Database options
- p.add_option('--database', dest='database', metavar='FILE',
- help="Path to the database file (%default).",
- default=os.path.join('example', 'inventory.db'))
-
- self.options, args = p.parse_args()
- p.destroy()
-
- if self.options.test == True:
- _test()
- sys.exit(0)
-
- if self.options.verbose:
- self.loglevel = 'DEBUG'
-
- # get self.options for httpserver
- if self.options.secure != None:
- split = self.options.secure.split(':', 1)
- assert len(split) == 2, (
- "Invalid secure argument '%s'" % self.options.secure)
- self.pkey_file = split[0]
- self.cert_file = split[1]
-
- # HACK! to ensure we *always* get utf-8 output
- #reload(sys)
- #sys.setdefaultencoding('utf-8')
-
- dirname,filename = os.path.split(
- os.path.abspath(self.options.database))
- static_dir = os.path.abspath(self.options.static)
- MSDS_dir = os.path.join(static_dir, 'MSDS')
- template_dir = os.path.abspath(self.options.template)
- doc_dir = os.path.abspath(self.options.doc)
- db = TextDB(filename=filename, current_dir=dirname)
- MSDS_manager = MSDSManager(db=db, dir=MSDS_dir)
- docgen = DocGen(db=db, doc_root=doc_dir)
- docs = Docs(db=db, docgen=docgen, template_root=template_dir)
- server = Server(db=db, MSDS_manager=MSDS_manager, docs=docs,
- template_root=template_dir, static_dir=static_dir,
- static_url='/static')
-
- if cherrypy.__version__.startswith('3.'):
- cherrypy.config.update({ # http://www.cherrypy.org/wiki/ConfigAPI
- 'server.socket_host': self.options.address,
- 'server.socket_port': self.options.port,
- 'tools.decode.on': True,
- 'tools.encode.on': True,
- 'tools.encode.encoding': 'utf8',
- 'tools.staticdir.root': static_dir,
- })
- digest_auth = None
- if cherrypy.__version__.startswith('3.2'):
- try:
- get_ha1 = cherrypy.lib.auth_digest.get_ha1_file_htdigest(
- self.options.htaccess)
- except IOError:
- pass
- else:
- digest_auth = {
- 'tools.auth_digest.on': True,
- 'tools.auth_digest.realm': 'chemdb',
- 'tools.auth_digest.get_ha1': get_ha1,
- 'tools.auth_digest.key': str(uuid.uuid4()),
- }
- else:
- passwds = {}
- try:
- with open(self.options.htaccess, 'r') as f:
- for line in f:
- user,realm,ha1 = line.strip().split(':')
- passwds[user] = ha1 # use the ha1 as the password
- except IOError:
- pass
- else:
- digest_auth = {
- 'tools.digest_auth.on': True,
- 'tools.digest_auth.realm': 'chemdb',
- 'tools.digest_auth.users': passwds,
- }
- app_config = {
- '/static': {
- 'tools.staticdir.on': True,
- 'tools.staticdir.dir': '',
- },
- }
- if digest_auth != None:
- for url in ['edit']:
- app_config[url] = digest_auth
- self.server = server
- self.app_config = app_config
- else:
- raise NotImplementedError(
- 'Unsupported CherryPy version %s' % cherrypy.__version__)
-
-
-def _test():
- import doctest
- doctest.testmod()
--- /dev/null
+/* <body> */
+
+body {
+ background: #eee;
+}
+
+.fullclear {
+ width:100%;
+ height:1px;
+ margin:0;
+ padding:0;
+ clear:both;
+}
+
+/* </body> */
+
+/* <div id="navigation"> */
+
+#navigation {
+ margin: 0;
+ padding: 4px 0 0 0;
+}
+
+#navigation ul {
+ list-style: none;
+ margin: 0;
+ padding: 0 0 0 12px;
+}
+
+#navigation ul li {
+ margin: 0;
+ margin-left: 2em;
+ padding: 0;
+ float: left;
+}
+
+#navigation ul li a {
+ display: block;
+ font-weight: bold;
+ text-decoration: none;
+ margin: 0;
+ padding: 5px 5px;
+ background-color: #eee;
+}
+
+#navigation ul li a:hover {
+ background-color: #ccc;
+}
+
+#navigation ul li.CurrentSection a {
+ background-color: #FFF;
+}
+
+#navigation ul li.CurrentSection a:hover {
+ background-color: #FFF;
+}
+
+#subnavigation ul li.CurrentSection a {
+ border-width: 0 0 1px 0;
+}
+
+/* </div id="navigation"> */
+
+/* <div id="content"> */
+
+#content {
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ padding-top: 1em;
+ padding-bottom: 1em;
+ background: #fff;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+table.centered {
+ margin: auto;
+}
+
+td, th {
+ padding-right: 1em;
+ text-align: left;
+}
+
+table.wide tr:nth-child(odd) {
+ background: #eee;
+}
+
+table.wide thead tr:nth-child(odd) {
+ background: #ccc;
+}
+
+span.number {
+ float: right; /* so decimal points match up */
+ font-family: monospace; /* so that n-place digits line up */
+ text-align: '.'; /* should work by itself, but browser support is bad */
+}
+
+span.positive {
+ color: green;
+}
+
+span.negative {
+ color: red;
+}
+
+/* </div id="content"> */
--- /dev/null
+<html>
+<head>
+ <title>{% block title %}ChemDB{% endblock %}</title>
+ <link rel="stylesheet" href="{{ STATIC_URL }}chemdb/style.css" />
+ <link rel="shortcut icon" href="{{ STATIC_URL }}chemdb/chemdb.ico" />
+</head>
+
+<body>
+ <div id="navigation">
+ {% block navigation %}
+ <ul>
+ <li><a href="/">Chemicals</a></li>
+ <li><a href="/doc/">Document generation</a></li>
+ <li><a href="/admin/">Admin</a></li>
+ </ul>
+ {% endblock %}
+ </div>
+ <div class="fullclear"></div>
+
+ <div id="content">
+ {% block content %}{% endblock %}
+ </div>
+ <div class="fullclear"></div>
+
+ <div id="footer">
+ <p>powered by <a href="http://blog.tremily.us/posts/ChemDB/">chemdb</a></p>
+ </div>
+</body>
+</html>
--- /dev/null
+{% extends "chemdb/base.html" %}
+
+{% block content %}
+<h1>{{ chemical.name }}</h1>
+
+{% block metadata %}
+<table class="metadata">
+ <tr><td>Abbreviation</td><td>{{ chemical.abbrev }}</td></tr>
+ <tr><td>CAS#</td><td>{{ chemical.cas_numbers }}</td></tr>
+ <tr><td>NFPA health rating</td><td>{{ chemical.health }}</td></tr>
+ <tr><td>NFPA fire rating</td><td>{{ chemical.fire }}</td></tr>
+ <tr><td>NFPA reactivity rating</td><td>{{ chemical.reactivity }}</td></tr>
+ <tr><td>NFPA special rating</td><td>{{ chemical.specials }}</td></tr>
+ <tr><td>Mutagen</td><td>{{ chemical.mutagen }}</td></tr>
+ <tr><td>Carcinogen</td><td>{{ chemical.carcinogen }}</td></tr>
+ <tr><td>Teratogen</td><td>{{ chemical.teratogen }}</td></tr>
+{% if chemical.msds %}
+ <tr><td>MSDS</td><td><a href="{{ chemical.msds.url }}">
+ {{ chemical.msds }}</a></td></tr>
+{% endif %}
+</table>
+{% endblock %}
+
+{% block note %}
+{% if chemical.note %}
+<div class="note">
+{{ chemical.note|safe }}
+</div>
+{% endif %}
+{% endblock %}
+{% endblock %}
--- /dev/null
+{% extends "chemdb/base.html" %}
+
+{% block title %}
+{{ title }}
+{% endblock %}
+
+{% block content %}
+<h1>{{ title }}</h1>
+{% if chemical_instances %}
+ <table>
+ <thead>
+ <tr>
+ <th>Chemical</th><th>CAS#</th><th>Location</th><th>Amount</th>
+ <th>Vendor</th><th>Catalog#</th><th>Recieved</th><th>Disposed</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for chemical_instance in chemical_instances %}
+ <tr>
+ <td><a href="{% url chemical chemical_instance.chemical.id %}">
+ {{ chemical_instance.chemical.abbrev }}</a></td>
+ <td>{{ chemical_instance.chemical.cas_numbers }}</td>
+ <td>{{ chemical_instance.location }}</td>
+ <td>{{ chemical_instance.amount }}</td>
+ <td>{{ chemical_instance.vendor }}</td>
+ <td>{{ chemical_instance.catalog }}</td>
+ <td>{{ chemical_instance.recieved }}</td>
+ <td>{{ chemical_instance.disposed|default:'' }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+{% else %}
+ <p>No chemicals are available.</p>
+{% endif %}
+{% endblock %}
--- /dev/null
+{% extends "chemdb/base.html" %}
+
+{% block title %}
+ Documents
+{% endblock %}
+
+{% block content %}
+<ul>
+ <li>
+ <a href="{% url doc target="inventory" %}">Inventory</a>
+ in accordance with the
+ <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">
+ Chemical Hygiene Plan Section E-7</a>.
+ </li>
+ <li>
+ <a href="{% url doc target="door" %}">Door warning</a>
+ in accordance with the Chemical Hygiene Plan Sections
+ <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">E-7</a>
+ and
+ <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe10">E-10</a>.
+ </li>
+</ul>
+<p>
+ For door warnings for a particular location or locations, use the
+ form below:
+</p>
+<form action="{% url doc target="inventory" %}" method="get">
+ {{ locations_form }}
+ <input type="submit" value="Submit" />
+</form>
+{% endblock %}
% You should have received a copy of the GNU General Public License
% along with ChemDB. If not, see <http://www.gnu.org/licenses/>.
-% Paper for posting on the lab door
-% as per
-% http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7
-% and
-% href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe10
+\documentclass[letterpaper]{article}
-% setup the margins
+{% block comment %}{% endblock %}
+
+% setup the margins,
\topmargin -0.5in
\headheight 0.0in
\headsep 0.0in
-\textheight 10in
+\textheight 9.5in % leave a bit of extra room for page numbers
\oddsidemargin -0.5in
\textwidth 7.5in
-\usepackage{nfpa_704} % \firediamond
-
% 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}
-
-\input{door_data}
-
-\vfill
+\usepackage{nfpa_704} % \firediamond
-{\headfont Contact}
-\vskip 10pt
-\contfont
-\input{contact}
-\vspacer
+{% block preamble %}{% endblock %}
-Generated \today\ by ChemDB.
+\begin{document}
-\end{center}
+{% block document %}{% endblock %}
\end{document}
--- /dev/null
+{% extends "chemdb/doc/base.tex" %}
+{% load latex %}
+
+{% block comment %}
+% Paper for posting on the lab door
+% as per
+% http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7
+% and
+% href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe10
+{% endblock %}
+
+{% block document %}
+\thispagestyle{empty} % suppress page numbering
+\sffamily % switch to sans-serif
+
+\begin{center}
+
+{\Huge
+ \firediamond{health={{ health }}, flammability={{ fire }},
+ reactivity={{ reactivity }}, {{ special|latex_special_args }} } }
+
+\vspacer
+
+\contfont
+\begin{tabular}{r r l}
+{% for title,instance_group in instance_groups %}
+{% if instance_group %}
+\multicolumn{3}{c}{\Tstrut {{ title }} } \\
+{% for chemical_instance in instance_group %}
+{{ chemical_instance.amount }} & {{ chemical_instance.id }} &
+ {{ chemical_instance.chemical.name|latex_safe }} \\
+{% endfor %}
+{% endif %}
+{% endfor %}
+\end{tabular}
+
+\vfill
+
+{\headfont Contact}
+\vskip 10pt
+\contfont
+\begin{tabular}{l}
+ W. Trevor King
+ 215-895-1818 \\
+ Disque Hall 927 \\
+\end{tabular}
+\vspacer
+
+Generated \today\ by ChemDB.
+
+\end{center}
+{% endblock %}
--- /dev/null
+{% extends "chemdb/doc/base.tex" %}
+{% load latex %}
+
+{% block comment %}
+% Chemical inventory as per
+% http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7
+{% endblock %}
+
+{% block preamble %}
+% 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}
+{% endblock %}
+
+{% block document %}
+
+\begin{center}
+{\headfont {{ title }} }\\
+\contfont
+Generated \today\ by ChemDB.\\
+\vskip 10pt
+\end{center}
+
+{% if chemical_instances %}
+\footnotesize
+\begin{longtable}{l l c c c c c c c c}
+% Header for the remaining page(s) of the table...
+ID & Name & Amount & H & F & R & O & M & C & T \\
+\hline
+\endhead{% for chemical_instance in chemical_instances %}
+{{ chemical_instance.id }} &
+ {{ chemical_instance.chemical.name|latex_safe }} &
+ {{ chemical_instance.amount }} &
+ {{ chemical_instance.chemical.health }} &
+ {{ chemical_instance.chemical.fire }} &
+ {{ chemical_instance.chemical.reactivity }} &
+ {{ chemical_instance.chemical.special.all|latex_specials }} &
+ {{ chemical_instance.chemical.mutagen|default:"" }} &
+ {{ chemical_instance.chemical.carcinogen|default:"" }} &
+ {{ chemical_instance.chemical.teratogen|default:"" }} \\
+{% endfor %}\end{longtable}
+{% else %}
+No chemicals are available.
+{% endif %}
+
+{% endblock %}
% ...
% SUBSTANTIATION: The committee expressed concern that for the
% relatively few chemicals requiring both an "OX" and "W" symbols in
-% the special hazards quadrant, there wouldn’t be enough room for
+% the special hazards quadrant, there wouldn't be enough room for
% both to appear in the prescribed sizes for clear visibility. The
% committee believed the "W" is the primary hazard and should be
% displayed inside the quadrant, with the "OX" outside the
+% Test the nfpa_704 package
+%
% Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
%
% This file is part of ChemDB.
--- /dev/null
+# Copyright
+
+"""Custom template tags and filters
+
+https://docs.djangoproject.com/en/dev/howto/custom-template-tags/
+"""
+
--- /dev/null
+# Copyright
+
+import sys as _sys
+
+from django import template as _template
+
+
+register = _template.Library()
+
+
+LATEX_REPLACEMENTS = [('%', '\%'), ('>', '$>$'), ('<', '$<$')]
+_SUPERSCRIPT_CODEPOINTS = {1: 0x00B9, 2: 0x00B2, 3: 0x00B3}
+for i in range(9):
+ subscript_codepoint = 0x2080 + i
+ superscript_codepoint = _SUPERSCRIPT_CODEPOINTS.get(i, 0x2070 + i)
+ if _sys.version_info >= (3,):
+ subscript = chr(subscript_codepoint)
+ superscript = chr(superscript_codepoint)
+ else:
+ subscript = unichr(subscript_codepoint)
+ superscript = unichr(superscript_codepoint)
+ LATEX_REPLACEMENTS.extend([
+ (subscript, '$_{{{}}}$'.format(i)),
+ (superscript, '$^{{{}}}$'.format(i)),
+ ])
+del i, subscript_codepoint, superscript_codepoint, subscript, superscript
+
+LATEX_SPECIALS = { # abbrev -> (latex, nfpa_704:firediamond argument)
+ 'OX': (r'\oxidizer', 'oxidizer'),
+ '-W-': (r'\nowater', 'nowater'),
+ 'SA': (r'\asphixiant', 'asphixiant'),
+ } # others default to ('{abbrev}', '{abbrev}')
+
+
+@register.filter
+def latex_safe(string):
+ for a,b in LATEX_REPLACEMENTS:
+ string = string.replace(a, b)
+ return string
+
+@register.filter
+def latex_specials(specials):
+ """Format specials for general LaTeX usage
+ """
+ special_abbrevs = set(special.abbrev for special in specials)
+ latex_specials = []
+ for abbrev in ['OX', '-W-', 'SA']:
+ if abbrev in special_abbrevs:
+ latex_specials.append(LATEX_SPECIALS[abbrev][0])
+ special_abbrevs.remove(abbrev)
+ if special_abbrevs: # leftovers:
+ special_abbrevs = sorted(special_abbrevs)
+ for abbrev in special_abbrevs:
+ latex_specials.append(
+ LATEX_SPECIALS.get(abbrev, ('{{{}}}'.format(abbrev), None))[0])
+ return '~'.join(latex_specials)
+
+@register.filter
+def latex_special_args(specials):
+ """Format specials for the NFPA firediamond.
+ """
+ special_abbrevs = set(special.abbrev for special in specials)
+ latex_specials = []
+ for abbrev in ['OX', '-W-', 'SA']:
+ if abbrev in special_abbrevs:
+ latex_specials.append(LATEX_SPECIALS[abbrev][1])
+ special_abbrevs.remove(abbrev)
+ if special_abbrevs: # leftovers:
+ special_abbrevs = sorted(special_abbrevs)
+ latex_specials.append('special={{{}}}'.format(
+ ','.join(special_abbrevs)))
+ return ', '.join(latex_specials)
--- /dev/null
+# Copyright
+
+"""Run tests with ``manage.py test``.
+"""
+
+from django.forms import ValidationError as _ValidationError
+from django.test import _TestCase as _TestCase
+from . import models as _models
+
+
+#class UnitTest(TestCase):
+# def setUp(self):
+# self.F = models.Unit(
+# name=u'degree Farenheit', abbrev=u'\u00b0F', type=u'temperature',
+# system=models.US, scale=1/1.8, offset=32)
+# self.gal = models.Unit(
+# name=u'gallon', abbrev=u'gal', type=u'volume',
+# system=models.US, scale=3.78541, offset=0)
+#
+# def test_conversion_from_si(self):
+# "Test from-SI conversion"
+# self.assertEqual(self.F.convert_from_si(0), 32)
+# self.assertEqual(self.F.convert_from_si(100), 212)
+# self.assertEqual(self.gal.convert_from_si(1), 0.26417217685798894)
+#
+# def test_conversion_to_si(self):
+# "Test to-SI conversion"
+# self.assertEqual(self.F.convert_to_si(32), 0)
+# self.assertEqual(self.F.convert_to_si(212), 100)
+# self.assertEqual(self.gal.convert_to_si(1), 3.78541)
+#
+# def test_formatting(self):
+# "Test amount formatting"
+# self.assertEqual(unicode(self.gal), u'gal')
+# self.assertEqual(unicode(self.F), u'\u00b0F')
+#
+#
+#class AmountTest(TestCase):
+# def setUp(self):
+# self.unit = models.Unit(
+# name=u'gallon', abbrev=u'gal', type=u'volume',
+# system=models.US, scale=3.78541, offset=0)
+# self.amount = models.Amount()
+#
+# def test_formatting(self):
+# "Test amount formatting"
+# for v,minv,maxv,unit,result in (
+# (None, None, None, None, u'-'),
+# (None, None, None, self.unit, u'- gal'),
+# (2, None, None, self.unit, u'2 gal'),
+# (2, 1.5, None, self.unit, u'1.5-2 gal'),
+# (2, None, 2.5, self.unit, u'2-2.5 gal'),
+# (2, 1.5, 2.5, self.unit, u'2 (1.5-2.5) gal'),
+# (None, 1.5, 2.5, self.unit, u'1.5-2.5 gal'),
+# ):
+# self.amount.unit = unit
+# self.amount.value = v
+# self.amount.min_value = minv
+# self.amount.max_value = maxv
+# self.assertEqual(self.amount.format_amount(), result)
+#
+# def test_invalid_formatting(self):
+# "Test amount formatting which raises errors"
+# for v,minv,maxv,unit,result in (
+# (None, 1.5, None, self.unit, u'2 gal'),
+# (None, None, 2.5, self.unit, u'2 gal'),
+# ):
+# self.amount.unit = unit
+# self.amount.value = v
+# self.amount.min_value = minv
+# self.amount.max_value = maxv
+# self.assertRaises(ValidationError, self.amount.format_amount)
+#
+# def test_validation(self):
+# "Test amount validation"
+# for valid,v,minv,maxv,unit in (
+# (True, None, None, None, None),
+# (True, None, None, None, self.unit),
+# (True, 2, None, None, self.unit),
+# (True, 2, 1.5, None, self.unit),
+# (True, 2, None, 2.5, self.unit),
+# (True, 2, 1.5, 2.5, self.unit),
+# (True, None, 1.5, 2.5, self.unit),
+# (False, None, 1.5, None, self.unit),
+# (False, None, None, 2.5, self.unit),
+# ):
+# self.amount.unit = unit
+# self.amount.value = v
+# self.amount.min_value = minv
+# self.amount.max_value = maxv
+# if valid:
+# self.amount.clean()
+# else:
+# self.assertRaises(ValidationError, self.amount.clean)
+
--- /dev/null
+# Copyright
+
+from django.conf import settings as _settings
+from django.conf.urls import defaults as _defaults
+from django.views import generic as _generic
+from django.contrib import admin as _admin
+
+# If you're not serving the static content via some other server
+from django.conf.urls.static import static as _static
+
+from . import models as _models
+from . import views as _views
+
+
+_admin.autodiscover()
+
+
+urlpatterns = _defaults.patterns(
+ '',
+ _defaults.url(r'^$', _views.static_context_list_view_factory(
+ extra_context={'title': 'Chemical instances'},
+ ).as_view(
+ queryset=_models.ChemicalInstance.objects.all(),
+ context_object_name='chemical_instances',
+ template_name='chemdb/chemical_instances.html'),
+ name='chemical_instances'),
+ _defaults.url(r'^chemical/(?P<pk>\d+)/$', _generic.DetailView.as_view(
+ model=_models.Chemical, template_name='chemdb/chemical.html'),
+ name='chemical'),
+ _defaults.url(r'^doc/(?P<target>\w*)$', _views.doc_page, name='doc'),
+
+ # Uncomment the admin/doc line below to enable admin documentation:
+ #_defaults.url(
+ # r'^admin/doc/', _defaults.include('django.contrib.admindocs.urls')),
+
+ # Uncomment the next line to enable the admin:
+ _defaults.url(
+ r'^admin/', _defaults.include(_admin.site.urls), name='admin'),
+ _defaults.url(
+ r'^grappelli/', _defaults.include('grappelli.urls'), name='admin'),
+
+ _defaults.url(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to',
+ kwargs={'url': _settings.STATIC_URL + 'chemdb.ico'}),
+ ) + _static(_settings.MEDIA_URL, document_root=_settings.MEDIA_ROOT)
+
--- /dev/null
+# Copyright
+
+import os.path as _os_path
+import re as _re
+
+from . import LOG as _LOG
+
+
+CAS_REGEXP = _re.compile('\A[0-9]{2,}[-][0-9]{2}[-][0-9]\Z')
+
+
+def valid_CASno(cas_string):
+ """Validate CAS numbers.
+
+ Check `N..NN-NN-N` format, and the `checksum digit`_ for valid CAS
+ number structure. for
+
+ .. math::
+ N_n .. N_4 N_3 - N_2 N_1 - R
+
+ The checksum digit is
+
+ .. math::
+ R = remainder([sum_{i=1}^n i N_i ] / 10 )
+
+ .. _checksum digit:
+ http://www.cas.org/expertise/cascontent/registry/checkdig.html
+
+ >>> valid_CASno('107-07-3')
+ True
+ >>> valid_CASno('107-08-3')
+ False
+ >>> valid_CASno('107-083')
+ False
+
+ Sometimes we don't have a CAS number, or a product will contain
+ secret, non-hazardous ingredients. Therefore we treat the strings
+ `na` and `+secret-non-hazardous` as valid CAS numbers.
+
+ >>> valid_CASno('na')
+ True
+ >>> valid_CASno('+secret-non-hazardous')
+ True
+ """
+ if cas_string in [None, 'na', '+secret-non-hazardous']:
+ return True
+ # check format,
+ # \A matches the start of the string
+ # \Z matches the end of the string
+ if CAS_REGEXP.match(cas_string) == None:
+ _LOG.debug("invalid CAS# format: '%s'".format(cas_string))
+ return False
+ # generate check digit
+ casdigs = [int(d) for d in ''.join(cas_string.split('-'))]
+ sumdigs = casdigs[:-1]
+ sumdigs.reverse()
+ s = sum([(i+1)*d for i,d in enumerate(sumdigs)])
+ check = s % 10
+ if check == casdigs[-1]:
+ return True
+ _LOG.debug("invalid CAS# check: '{}' (expected {})".format(
+ cas_string, check))
+ return False
+
+def sanitize_path(string):
+ for a,b in [(' ', '-'), ('..', '-')]:
+ string = string.replace(a, b)
+ return string
+
+def chemical_upload_to(instance, filename, storage=None):
+ basename,extension = _os_path.splitext(filename)
+ if extension not in ['.pdf', '.html', '.txt']:
+ raise ValueError(filename)
+ return 'msds/{}{}'.format(sanitize_path(instance.abbrev), extension)
--- /dev/null
+# Copyright
+
+from django import http as _http
+from django import shortcuts as _shortcuts
+from django import template as _template
+from django.views import generic as _generic
+
+from . import doc as _doc
+from . import forms as _forms
+from . import models as _models
+
+
+class StaticContextListView (_generic.ListView):
+ _name_counter = 0
+ def get_context_data(self, **kwargs):
+ context = super(StaticContextListView, self).get_context_data(**kwargs)
+ context.update(self._extra_context)
+ return context
+
+def static_context_list_view_factory(extra_context):
+ class_name = 'StaticContextListView_{:d}'.format(
+ StaticContextListView._name_counter)
+ StaticContextListView._name_counter += 1
+ class_bases = (StaticContextListView,)
+ class_dict = dict(StaticContextListView.__dict__)
+ new_class = type(class_name, class_bases, class_dict)
+ new_class._extra_context = extra_context
+ return new_class
+
+def doc_page(request, target=None):
+ context = _template.RequestContext(request)
+ if target in [None, '', 'index']:
+ context['locations_form'] = _forms.LocationsForm()
+ return _shortcuts.render_to_response('chemdb/doc.html', context)
+ elif target in ['inventory', 'door']:
+ pass
+ else:
+ raise _http.Http404()
+ locations = [_shortcuts.get_object_or_404(_models.Location, pk=int(id))
+ for id in request.GET.getlist('location')]
+ if locations:
+ chemical_instances = []
+ for location in locations:
+ chemical_instances.extend(location.chemical_instances.all())
+ chemical_instances.sort()
+ else:
+ chemical_instances = _models.ChemicalInstance.objects.all()
+ dg = _doc.DocGen(chemical_instances=chemical_instances)
+ if target == 'inventory':
+ pdf = dg.inventory(title='Inventory')
+ else:
+ pdf = dg.door_warning()
+ response = _http.HttpResponse(mimetype='application/pdf')
+ response['Content-Disposition'] = 'attachment; filename={}.pdf'.format(
+ target)
+ response.write(pdf)
+ return response
--- /dev/null
+# Copyright
--- /dev/null
+sqlite3.db
+templates
+++ /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 (CH3COOH) 500 ml 64-19-7 A35-500 Fisher 7/5/2001 acidic liquids 3 2 1
-1 Ammonium Persulfate ((NH4)2S2O8) 100 g 7727-54-0 15523-012 Life Tech. 1/16/2000 oxidizing solids 3 0 3 OX
-2 Glycerin Solution 50% (C3H5(OH)3) 500 ml 56-81-5,7647-01-0,7732-18-5 VW3410-2 VWR 1/17/2001 corrosive liquids 1 0 1 M T Labled H2 F0 R0 in NFPA diamond on the bottle. Corrosive. Avoid strong bases and oxidizers.
-3 Glycine (NH2CH2COOH) 500 g 56-40-6 15527-013 Life Tech. 1/16/2000 non-hazardous 1 0 0 Avoid strong oxidizers
-4 Phenol (C6H5OH) 100 g 108-95-2 P1037 Sigma 1/17/2006 reactive solids 3 2 0 M C T Avoid strong oxidizers (especially calcium hypochlorite), acids, and halogens.
-5 Sodium Chloride (NaCl) 500 g 7647-14-5 JT4058-1 VWR/Fisher 10/22/2000 non-hazardous 1 0 0
-6 Sodium Hydroxide (NaOH) 50% (w/w) solution 500 ml 1310-73-2,7732-18-5 JT3727-1 VWR/JTBaker 10/22/2000 basic liquids 3 0 2 W
--- /dev/null
+#!/usr/bin/env python
+from django.core.management import execute_manager
+import imp
+try:
+ imp.find_module('settings') # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__)
+ sys.exit(1)
+
+import settings
+
+if __name__ == "__main__":
+ execute_manager(settings)
--- /dev/null
+# Copyright
+
+# Django settings for web project.
+
+import os
+import os.path
+
+
+# This is where we'll put the non-Python portions of the app
+# (e.g. templates) and store state information (e.g. the SQLite
+# database). This should be an absolute path.
+DEFAULT_DATA_DIRECTORY = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), 'data'))
+DATA_DIRECTORY = os.environ.get('DJANGO_CHEMDB_DATA', DEFAULT_DATA_DIRECTORY)
+
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ ('W. Trevor King', 'wking@drexel.edu'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+ 'default': {
+ #'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(DATA_DIRECTORY, 'sqlite3.db'), # Or path to database file if using sqlite3.
+ 'USER': '', # Not used with sqlite3.
+ 'PASSWORD': '', # Not used with sqlite3.
+ 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
+ 'PORT': '', # Set to empty string for default. Not used with sqlite3.
+ }
+}
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = None # 'America/New_York'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/home/media/media.lawrence.com/media/"
+MEDIA_ROOT = os.path.join(DATA_DIRECTORY, 'media')
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
+MEDIA_URL = '/media/'
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/home/media/media.lawrence.com/static/"
+STATIC_ROOT = os.path.join(DATA_DIRECTORY, 'static')
+
+# URL prefix for static files.
+# Example: "http://media.lawrence.com/static/"
+STATIC_URL = '/static/'
+
+# URL prefix for admin static files -- CSS, JavaScript and images.
+# Make sure to use a trailing slash.
+# Examples: "http://foo.com/static/admin/", "/static/admin/".
+#ADMIN_MEDIA_PREFIX = STATIC_URL + 'admin/'
+ADMIN_MEDIA_PREFIX = STATIC_URL + 'grappelli/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+ # Put strings here, like "/home/html/static" or "C:/www/django/static".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
+)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'sywBDVfqIMc1CQ43bu4LF2Rj0dDfcMsBVtshxZ65OpolclSKvhbXq8HXVStGpx6R'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+# 'django.template.loaders.eggs.Loader',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+)
+
+ROOT_URLCONF = 'chemdb.urls'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+ os.path.join(DATA_DIRECTORY, 'templates'),
+)
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ # Uncomment the next line to enable the admin:
+ 'grappelli',
+ 'django.contrib.admin',
+ # Uncomment the next line to enable admin documentation:
+ # 'django.contrib.admindocs',
+ 'chemdb',
+)
+
+# Required for render_table
+# http://django-tables2.readthedocs.org/en/latest/#render-table
+TEMPLATE_CONTEXT_PROCESSORS = (
+ "django.contrib.auth.context_processors.auth",
+ "django.core.context_processors.debug",
+ "django.core.context_processors.i18n",
+ "django.core.context_processors.media",
+ "django.core.context_processors.static",
+ "django.contrib.messages.context_processors.messages",
+ "django.core.context_processors.request",
+)
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'handlers': {
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'class': 'django.utils.log.AdminEmailHandler'
+ }
+ },
+ 'loggers': {
+ 'django.request': {
+ 'handlers': ['mail_admins'],
+ 'level': 'ERROR',
+ 'propagate': True,
+ },
+ }
+}
+++ /dev/null
-<html>
- <head>
- </head>
- <body>
- Acetic Acid MSDS.
- </body>
-</html>
--- /dev/null
+# Copyright (C) 2012 W. Trevor King
+#
+# This file is part of update-copyright.
+#
+# update-copyright is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# update-copyright is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with update-copyright. If not, see
+# <http://www.gnu.org/licenses/>.
+
+"Track chemical inventories and produce inventories and door warnings."
+
+import codecs as _codecs
+from distutils.core import setup as _setup
+import os.path as _os_path
+
+from chemdb import __version__
+
+
+_this_dir = _os_path.dirname(__file__)
+
+_setup(
+ name='chemdb',
+ version=__version__,
+ maintainer='W. Trevor King',
+ maintainer_email='wking@drexel.edu',
+ url='http://blog.tremily.us/posts/ChemDB/',
+ download_url='http://git.tremily.us/?p=chemdb.git;a=snapshot;h={};sf=tgz'.format(__version__),
+ license='GNU General Public License (GPL)',
+ platforms=['all'],
+ description=__doc__,
+ long_description=_codecs.open(
+ _os_path.join(_this_dir, 'README'), 'r', encoding='utf-8').read(),
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Framework :: Django',
+ 'Intended Audience :: Science/Research',
+ 'Operating System :: OS Independent',
+ 'License :: OSI Approved :: GNU General Public License (GPL)',
+ 'Programming Language :: Python',
+ 'Topic :: Database',
+ 'Topic :: Scientific/Engineering :: Chemistry',
+ ],
+ packages=['chemdb', 'chemdb.templatetags'],
+ provides=['chemdb', 'chemdb.templatetags'],
+ package_data = {
+ 'chemdb': [
+ 'fixtures/*',
+ 'static/chemdb/*',
+ 'templates/chemdb/*.html',
+ 'templates/chemdb/doc/*',
+ ]},
+ )
+++ /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 `nfpa_704.sty' package contains LaTeX code for the \firediamond macro.
-For usage samples, see `nfpa_704.tex'.
+++ /dev/null
-\begin{tabular}{l}
- Professor Guoliang Yang\\
- 215-895-6669 \\
- Disque Hall 926 \\
-\end{tabular}
+++ /dev/null
-% Copyright (C) 2008-2010 W. Trevor King <wking@drexel.edu>
-%
-% This file is part of ChemDB.
-%
-% ChemDB is free software: you can redistribute it and/or modify it
-% under the terms of the GNU General Public License as published by the
-% Free Software Foundation, either version 3 of the License, or (at your
-% option) any later version.
-%
-% ChemDB is distributed in the hope that it will be useful,
-% but WITHOUT ANY WARRANTY; without even the implied warranty of
-% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-% GNU General Public License for more details.
-%
-% You should have received a copy of the GNU General Public License
-% along with ChemDB. If not, see <http://www.gnu.org/licenses/>.
-
-% Chemical inventory as per
-% http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7
-
-% 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
-
-\usepackage{nfpa_704} % \firediamond
-
-% 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 \input{inventory_title}}\\
-\contfont
-Generated \today\ by ChemDB.\\
-\vskip 10pt
-\end{center}
-
-\footnotesize
-\input{inventory_data}
-
-\end{document}
+++ /dev/null
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
- xml:lang="en">
-
-<html>
- <head>
- <title>ChemDB</title>
-<!--
- <link rel="stylesheet" type="text/css" media="screen"
- href="/static/style.css"/>
--->
- </head>
-
- <body>
- <div id="main-pane">
- <div id="header" class="inside-main-pane">
- <h1><a href="./">ChemDB</a></h1>
- </div>
- <div id="content-pane" class="inside-main-pane">
- <h1>{% block page_title %}{% endblock %}</h1>
- {% block content %}{% endblock %}
- </div>
- <div id="footer" class="inside-main-pane">
- <p>
- Powered by
- <a href="http://www.physics.drexel.edu/">Trevor King's</a>
- <a href="http://www.physics.drexel.edu/~wking/code/git/git.php?p=chemdb.git">ChemDB</a>.
- Built using <a href="http://cherrypy.org/">CherryPy</a>
- and <a href="http://jinja.pocoo.org/2/">Jinja2</a>.
- </p>
- </div>
- </div>
- </body>
-</html>
+++ /dev/null
-{% extends "base.html" %}
-
-{% block page_title %}
- Documents
-{% endblock %}
-
-{% block content %}
-<ul>
- <li>
- <a href="/docs/inventory.pdf">Inventory</a>
- in accordance with the
- <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">
- Chemical Hygiene Plan Section E-7</a>.
- </li>
- <li>
- <a href="/docs/door_warning.pdf">Door warning</a>
- in accordance with the Chemical Hygiene Plan Sections
- <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe7">E-7</a>
- and
- <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm#chpe10">E-10</a>.
- </li>
-</ul>
-<p>
- For door warnings for subsections of the whole room, please give a
- location regexp in the form below. For example: ".*liquids" or
- "refrigerator".
-</p>
-<form action="door_warning.pdf" method="get">
- <table>
- <tr><td>Location regexp</td><td>:</td>
- <td><input type="text" size="50" id="location" name="location"></td></tr>
- </table>
- <input type="submit" value="Submit" />
-</form>
-{% endblock %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block page_title %}
- Edit {{ record['ID'] }}
-{% endblock %}
-
-{% block content %}
- <h1>Editing: {{ record['ID'] }}</h1>
- <!-- the form encoding type 'enctype="multipart/form-data">
- is only required because of the MSDS file-upload field. -->
- <form action="" method="post" enctype="multipart/form-data">
- <table>
- <tr>
- <th>Field</td><td>Value</td>
- </tr>
- {% for field in fields %}
- <tr>
- <td><label for="{{ escape(field, quote=True) }}">
- {{ escape(long_fields[field]) }}</label></td>
- <td><input type="text" size="{{ entry_width }}"
- id="{{ escape(field, quote=True) }}"
- name="{{ escape(field, quote=True) }}"
- value="{{ escape(record[field], quote=True) }}">
- </td>
- </tr>
- {% endfor %}
- {% if MSDSs %}
- <tr>
- <td><label for="MSDS source">MSDS source</label></td>
- <td>
- <input type="radio" name="MSDS source" value="upload"
- checkked="checked">Upload
- <input type="radio" name="MSDS source" value="share">Share
- </td>
- </tr>
- <tr>
- <td><label for="MSDS upload">Upload file</label></td>
- <td><input type="file" size="{{ entry_width }}"
- id="MSDS upload" name="MSDS upload"
- accept="text/plain,text/html,application/pdf" value="">
- </td>
- </tr>
- <tr>
- <td><label for="MSDS share">Use an already uploaded MSDS</label></td>
- <td>
- <select size="1" id="MSDS share" name="MSDS share">
- {% for MSDS in MSDSs %}
- <option value="{{ MSDS['ID'] }}">
- {{ MSDS['ID'] }}: {{ escape(MSDS['Name']) }}</option>
- {% endfor %}
- </select>
- </td>
- </tr>
- {% endif %}
- </table>
- <input type="submit" value="Submit" />
- </form>
-{% endblock %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block page_title %}
- Index
-{% endblock %}
-
-{% block content %}
- <p>See <a href="http://www.drexelsafetyandhealth.com/lab-chem.htm">the
- rules</a> for more information. See
- the <a href="/docs/">docs</a> page to generate required
- documents.
- </p>
- <p><a href="edit">Add entry</a></p>
- {% if records %}
- <table id="record-list">
- <tr>
- {% for field in fields %}
- <td>{{ field }}</td>
- {% endfor %}
- </tr>
- {% for record in records %}
- <tr>
- {% for field in fields %}
- {% if field == 'ID' %}
- <td>
- <a href="?id={{ record['ID'] }}">
- {{ record[field] }}
- </a>
- </td>
- {% else %}
- <td>{{ record[field] }}</td>
- {% endif %}
- {% endfor %}
- </tr>
- {% endfor %}
- </table>
- {% endif %}
- <p><a href="edit">Add entry</a></p>
-{% endblock %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block page_title %}
- {{ record['ID'] }}
-{% endblock %}
-
-{% block content %}
- <h1>{{ record['ID'] }}</h1>
- <table>
- <tr>
- <th>Field</td><td>Value</td>
- </tr>
- {% for field in fields %}
- <tr>
- <td>{{ escape(long_fields[field]) }}</td>
- <td>{{ escape(record[field]) }}</td>
- </tr>
- {% endfor %}
- <tr>
- <td><label for="MSDS">MSDS</label></td>
- <td>{{ MSDS }}</td>
- </tr>
- </table>
- <p><a href="edit?id={{ record['ID'] }}">
- Edit record</a></p>
-{% endblock %}