some help text and css changes
authorartunit <artunit@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Mon, 17 Aug 2009 14:23:07 +0000 (14:23 +0000)
committerartunit <artunit@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Mon, 17 Aug 2009 14:23:07 +0000 (14:23 +0000)
git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@621 6d9bc8c9-1ec2-4278-b937-99fde70a366f

conifer/libsystems/sip/sipclient.py
conifer/settings.py
conifer/static/main.css
conifer/templates/item/item_add_cat_search.xhtml

index ee887f1..4bce1f8 100644 (file)
-# Small portions are borrowed from David Fiander's acstest.py, in the
-# openncip project. David's license is below:
-
-# Copyright (C) 2006-2008  Georgia Public Library Service
-# 
-# Author: David J. Fiander
-# 
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of version 2 of the GNU General Public
-# License as published by the Free Software Foundation.
-# 
-# This program 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 this program; if not, write to the Free
-# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
-# MA 02111-1307 USA
-
-
-from sipconstants import *
-import socket
-import sys
-from datetime import datetime
-import re
-
-DEBUG = True
-
-# ------------------------------------------------------------
-# helper functions
-
-def split_n(n):
-    """Return a function that splits a string into two parts at index N."""
-    return lambda s: (s[:n], s[n:])
-
-split2 = split_n(2)
-
-
-
-
-# ------------------------------------------------------------
-# Messages
-
-# First we build up a little language for defining SIP messages, so
-# that we can define the protocol in a declarative style.
-
-
-class basefield(object): 
-
-    def encode(self, dct):
-        """Take a dict, and return the wire representation of this field."""
-        raise NotImplementedError, repr(self)
-
-    def decode(self, bytes):
-        """
-        Take a wire representation and return a pair (V,R) where V is
-        the translated value of the current field, and R is the
-        remaining bytes after the field has been read. If this is an
-        optional field, then decode should return None for V, and
-        return the input bytes for R.
-        """
-        raise NotImplementedError, repr(self)
-
-
-class field(basefield):
-
-    def __init__(self, name, code, width=None):
-        self.name = name 
-        self.code = code
-        self.width = None       # don't use this yet.
-
-    def encode(self, dct):
-        return '%s%s|' % (self.code, dct.get(self.name, ''))
-
-    def decode(self, bytes):
-        bcode, rest = split2(bytes)
-        if bcode != self.code:
-            raise 'BadDecode', \
-                'Wrong field! Expected %r (%s) got %r (%s), in %r.' % (
-                    self.code, lookup_constant(self.code),
-                    bcode, lookup_constant(bcode),
-                    bytes)
-        data, rest = rest.split('|', 1)
-        return data, rest
-
-
-class optfield(field):          # an optional field
-
-    def decode(self, bytes):
-        tmp = bytes + '  '
-        bcode, rest = split2(tmp)
-        if bcode == self.code:
-            return field.decode(self, bytes)
-        else:
-            return None, bytes
-
-        
-class charfield(basefield):
-
-    def __init__(self, name, width=None, default=None):
-        self.name = name
-        self.dflt = str(default)
-        self.width = width or len(self.dflt) # give at least one
-        self.pad = ' ' * self.width
-
-        self.decode = split_n(self.width)
-
-    def encode(self, dct):
-        v = dct.get(self.name, self.dflt)
-        assert v is not None
-        return ('%s%s' % (self.pad, v))[-self.width:]
-
-
-class yn(basefield):
-    def __init__(self, name):
-        self.name = name
-
-    def encode(self, dct):
-        return 'NY'[bool(dct.get(self.name))]
-
-    def decode(self, bytes):
-        return (bytes[0] == 'Y'), bytes[1:]
-
-
-class localtime(charfield):
-    def __init__(self, name):
-        self.name = name
-        self.width = 18
-
-    def encode(self, dct):
-        return datetime.now().strftime('%Y%m%d    %H%M%S')
-
-    def decode(self, bytes):
-        return split_n(self.width)(bytes)
-
-RAW = -55
-class raw(basefield):
-    name = 'raw'
-    # for debugging.
-    def decode(self, bytes):
-        return bytes, '\r'
-
-# We define a protocol Message as a list of fields. For now,
-# message(A, B, C) is equivalent to the tuple (A,B,C).
-
-message = lambda *args: args
-
-# Encoding a message on to the wire. Args is a dict of field-values.
-
-def encode_msg(msg, args):
-    out = []
-    add = out.append
-    for thing in msg:
-        if isinstance(thing, basefield):
-            add(thing.encode(args))
-        else:
-            add(str(thing))
-    return ''.join(out)
-
-# Decoding from the wire:
-
-def decode_msg(msg, bytes):
-    out = {}
-    add = out.__setitem__
-    rest = bytes
-    
-    # Proper 'fields' have variable position in the tail of the
-    # message. So we treat them differently.
-    varposn = set([p for p in msg if isinstance(p, field)])
-    varlookup = dict((x.code, x) for x in varposn)
-    fixedposn = [p for p in msg if not p in varposn]
-    
-    for part in fixedposn:
-        if isinstance(part, basefield):
-            good, rest = part.decode(rest)
-            if good is not None:
-                add(part.name, good)
-        else:
-            v = str(part)
-            good, rest = rest[:len(v)], rest[len(v):]
-            assert v == good
-        if DEBUG: print '%s == %r\n==== %r' % (getattr(part, 'name',''), good, rest)
-
-    # Now we take what's left, chunk it, and try to resolve each one
-    # against a variable-position field.
-    segments = re.findall(r'(.*?\|)', rest)
-    
-    if DEBUG: print segments
-
-    for segment in segments:
-        fld = varlookup.get(segment[:2])
-        if fld:
-            good, rest = fld.decode(segment)
-            add(fld.name, good)
-            varposn.remove(fld)
-        else:
-            raise 'FieldNotProcessed: %s, %s' % (segment, lookup_constant(segment[:2]))
-
-    # Let's make sure that any "required" fields were not missing.
-    notpresent = set(f for f in varposn if not isinstance(f, optfield))
-    if notpresent:
-        for f in notpresent:
-            print 'MISSING: %-12s %s %s' % (f.name, f.code, lookup_constant(f.code))
-        raise 'MandatoryFieldsNotPresent'
-
-    return out
-
-# The SIP checksum. Borrowed from djfiander.        
-
-def checksum(msg):
-    return '%04X' % ((0 - sum(map(ord, msg))) & 0xFFFF)
-
-
-#------------------------------------------------------------
-# SIP Message Definitions
-
-# some common fields
-
-
-fld_localtime     = localtime('localtime')
-fld_INST_ID       = field('inst', FID_INST_ID)
-fld_ITEM_ID       = field('item', FID_ITEM_ID)
-fld_PATRON_ID     = field('patron', FID_PATRON_ID)
-ofld_TERMINAL_PWD = optfield('termpwd', FID_TERMINAL_PWD)
-fld_proto_version = charfield('version', default='2.00')
-ofld_print_line    = optfield('print_line', FID_PRINT_LINE)
-ofld_screen_msg    = optfield('screenmsg', FID_SCREEN_MSG)
-
-MESSAGES = {
-    LOGIN : message(
-            LOGIN, 
-            '00',
-            field('uid', FID_LOGIN_UID),
-            field('pwd', FID_LOGIN_PWD),
-            field('locn', FID_LOCATION_CODE)),
-
-    LOGIN_RESP : message(
-            LOGIN_RESP, 
-            charfield('ok', width=1)),
-
-    SC_STATUS : message(
-            SC_STATUS, 
-            charfield('online', default='1'),
-            charfield('width', default='040'),
-            fld_proto_version),
-
-    ACS_STATUS : message(
-            ACS_STATUS,
-            yn('online'),
-            yn('checkin_OK'),
-            yn('checkout_OK'),
-            yn('renewal_OK'),
-            yn('status_update_OK'),
-            yn('offline_OK'),
-            charfield('timeout', default='01'),
-            charfield('retries', default='9999'),
-            fld_localtime,
-            charfield('protocol', default='2.00'),
-            fld_INST_ID,
-            optfield('patron_id', FID_PATRON_ID),
-            optfield('item_id', FID_ITEM_ID),
-            optfield('terminal_pwd', FID_TERMINAL_PWD),
-            
-            optfield('instname', FID_LIBRARY_NAME),
-            field('supported', FID_SUPPORTED_MSGS),
-            optfield('ttylocn', FID_TERMINAL_LOCN),
-            ofld_screen_msg,
-            ofld_print_line),
-    PATRON_INFO : message(
-            PATRON_INFO,
-            charfield('lang', width=3, default=1),
-            fld_localtime,
-            charfield('holditemsreq', default='Y         '),
-            fld_INST_ID,
-            fld_PATRON_ID,
-            ofld_TERMINAL_PWD,
-            optfield('patronpwd', FID_PATRON_PWD),
-            optfield('startitem', FID_START_ITEM, width=5),
-            optfield('enditem', FID_END_ITEM, width=5)),
-            
-    PATRON_INFO_RESP : message(
-            PATRON_INFO_RESP,
-            charfield('hmmm', width=14),
-            charfield('lang', width=3, default=1),
-            fld_localtime,
-            charfield('onhold', width=4),
-            charfield('overdue', width=4),
-            charfield('charged', width=4),
-            charfield('fine', width=4),
-            charfield('recall', width=4),
-            charfield('unavail_holds', width=4),
-            fld_INST_ID,
-            ofld_screen_msg,
-            ofld_print_line,
-            optfield('instname', FID_LIBRARY_NAME),
-            fld_PATRON_ID,
-            field('personal', FID_PERSONAL_NAME),
-
-            optfield('hold_limit', FID_HOLD_ITEMS_LMT, width=4),
-            optfield('overdue_limit', FID_OVERDUE_ITEMS_LMT, width=4),
-            optfield('charged_limit', FID_OVERDUE_ITEMS_LMT, width=4),
-
-            optfield('hold_items', FID_HOLD_ITEMS),
-            optfield('valid_patron_pwd', FID_VALID_PATRON_PWD),
-            
-            optfield('valid_patron', FID_VALID_PATRON),
-            optfield('currency', FID_CURRENCY),
-            optfield('fee_amt', FID_FEE_AMT),
-            optfield('fee_limit', FID_FEE_LMT),
-            optfield('home_addr', FID_HOME_ADDR),
-            optfield('email', FID_EMAIL),
-            optfield('home_phone', FID_HOME_PHONE),
-            optfield('patron_birthdate', FID_PATRON_BIRTHDATE),
-            optfield('patron_class', FID_PATRON_CLASS),
-            optfield('inet_profile', FID_INET_PROFILE),
-            optfield('home_library', FID_HOME_LIBRARY)),
-
-    END_PATRON_SESSION : message(
-            END_PATRON_SESSION,
-            fld_localtime,
-            field('inst', FID_INST_ID),
-            field('patron', FID_PATRON_ID)),
-
-    END_SESSION_RESP : message(
-            END_SESSION_RESP,
-            yn('session_ended'),
-            fld_localtime,
-            fld_INST_ID,
-            fld_PATRON_ID,
-            ofld_print_line,
-            ofld_screen_msg),
-
-    CHECKOUT: message(
-        CHECKOUT,
-        yn('renewals_OK'),
-        yn('no_block'),
-        fld_localtime,
-        fld_localtime,
-        field('inst', FID_INST_ID),
-        field('patron', FID_PATRON_ID),
-        field('item', FID_ITEM_ID),
-        ),
-
-    CHECKOUT_RESP: message(
-        CHECKOUT_RESP,
-        charfield('ok', width=1),
-        yn('is_renewal'),
-        yn('is_magnetic'),
-        yn('desensitize'),
-        fld_localtime,
-        field('inst', FID_INST_ID),
-        field('patron', FID_PATRON_ID),
-        field('item', FID_ITEM_ID),
-        field('due', FID_DUE_DATE),
-        field('title', FID_TITLE_ID),
-        optfield('media_type_code', FID_MEDIA_TYPE),
-        optfield('is_valid_patron', FID_VALID_PATRON),
-        ofld_print_line,
-        ofld_screen_msg),
-
-    CHECKIN: message(
-        CHECKIN,
-        yn('is_retry'),
-        fld_localtime,
-        fld_localtime,
-        field('item', FID_ITEM_ID),
-        field('location', FID_CURRENT_LOCN),
-        field('inst', FID_INST_ID),
-        ofld_TERMINAL_PWD,
-        ),
-
-    CHECKIN_RESP: message(
-        CHECKIN_RESP,
-        charfield('ok', width=1),
-        yn('resensitize'),
-        yn('is_magnetic'),
-        yn('alert'),
-        fld_localtime,
-        fld_INST_ID,
-        optfield('patron', FID_PATRON_ID),
-        field('item', FID_ITEM_ID),
-        field('title', FID_TITLE_ID),
-        optfield('media_type_code', FID_MEDIA_TYPE),
-        optfield('perm_locn', FID_PERM_LOCN),
-        optfield('due', FID_DUE_DATE),
-        ofld_print_line,
-        ofld_screen_msg,
-        ),
-#         yn('is_retry'),
-#         fld_localtime,
-#         fld_localtime,
-#         field('item', FID_ITEM_ID),
-#         field('location', FID_CURRENT_LOCN),
-#         ofld_TERMINAL_PWD,
-#        ),
-
-    ITEM_INFORMATION : message(
-            ITEM_INFORMATION,
-            fld_localtime,
-            fld_INST_ID,
-            fld_ITEM_ID,
-            ofld_TERMINAL_PWD),
-
-    ITEM_INFO_RESP : message(
-            ITEM_INFO_RESP,
-            charfield('circstat', width=2),
-            charfield('security', width=2),
-            charfield('feetype', width=2),
-            fld_localtime,
-            fld_ITEM_ID,
-            field('title', FID_TITLE_ID),
-            optfield('mediatype', FID_MEDIA_TYPE),
-            optfield('perm_locn', FID_PERM_LOCN),
-            optfield('current_locn', FID_CURRENT_LOCN),
-            optfield('item_props', FID_ITEM_PROPS),
-            optfield('currency', FID_CURRENCY),
-            optfield('fee', FID_FEE_AMT),
-            optfield('owner', FID_OWNER),
-            optfield('hold_queue_len', FID_HOLD_QUEUE_LEN),
-            optfield('due_date', FID_DUE_DATE),
-
-            optfield('recall_date', FID_RECALL_DATE),
-            optfield('hold_pickup_date', FID_HOLD_PICKUP_DATE),
-            ofld_screen_msg,
-            ofld_print_line),
-            
-    RAW : message(raw()),
-}
-
-
-class SipClient(object):
-    def __init__(self, host, port, error_detect=False):
-        self.hostport = (host, port)
-        self.error_detect = error_detect
-        self.connect()
-
-    def connect(self):
-        so = socket.socket()
-        so.connect(self.hostport)
-        self.socket = so
-        self.seqno = self.error_detect and 1 or 0
-
-    def close(self):
-        # fixme, do SIP close first.
-        self.socket.close()
-
-    def send(self, outmsg, inmsg, args=None):
-        msg_template = MESSAGES[outmsg]
-        resp_template = MESSAGES[inmsg]
-        msg = encode_msg(msg_template, args or {})
-        if self.error_detect:
-            # add the checksum
-            msg += 'AY%dAZ' % (self.seqno % 10)
-            self.seqno += 1
-            msg += checksum(msg)
-        msg += '\r'
-        if DEBUG: print '>>> %r' % msg
-        self.socket.send(msg)
-        resp = self.socket.recv(1000)
-        if DEBUG: print '<<< %r' % resp
-        return decode_msg(resp_template, resp)
-        
-
-    # --------------------------------------------------
-    # Common protocol methods
-
-    def login(self, uid, pwd, locn):
-        msg = self.send(LOGIN, LOGIN_RESP, 
-                        dict(uid=uid, pwd=pwd, locn=locn))
-        return msg.get('ok') == '1'
-
-    def status(self):
-        return self.send(SC_STATUS, ACS_STATUS)
-
-    def patron_info(self, barcode):
-        msg = self.send(PATRON_INFO,PATRON_INFO_RESP,
-                        {'patron':barcode,
-                         'startitem':1, 'enditem':2})
-        # fixme, this may not be the best test of okayness
-        msg['success'] = msg.get('valid_patron') == 'Y'
-        return msg
-
-    def checkout(self, patron, item, inst=''):
-        msg = self.send(CHECKOUT, CHECKOUT_RESP,
-                        {'patron':patron,
-                         'inst': inst,
-                         'item':item})
-        msg['media_type'] = MEDIA_TYPE_TABLE.get(msg.get('media_type_code'))
-        msg['success'] = msg.get('ok') == '1'
-        return msg
-
-    def checkin(self, item, institution='', location=''):
-        msg = self.send(CHECKIN, CHECKIN_RESP,
-                        {'inst': institution,
-                         'location':location,
-                         'is_retry':False,
-                         'item':item})
-        msg['success'] = msg.get('ok') == '1'
-        return msg
-
-    def item_info(self, barcode):
-        msg = self.send(ITEM_INFORMATION, ITEM_INFO_RESP,
-                        {'item':barcode})
-        msg['available'] = msg['circstat'] == '03'
-        msg['status'] = ITEM_STATUS_TABLE[msg['circstat']]
-        return msg
-
-
-# ------------------------------------------------------------
-# Django stuff. Optional.
-
-try:
-    from django.conf import settings
-    def sip_connection():
-        sip = SipClient(*settings.SIP_HOST)
-        if not sip.login(*settings.SIP_CREDENTIALS):
-            raise 'SipLoginError'
-        return sip
-
-    # decorator
-    def SIP(fn):
-        def f(*args, **kwargs):
-            conn = sip_connection()
-            resp = fn(conn, *args, **kwargs)
-            conn.close()
-            return resp
-        return f
-
-except ImportError:
-    pass
-
-
-# ------------------------------------------------------------
-# Test code.
-
-if __name__ == '__main__':
-    from pprint import pprint
-
-    sip = SipClient('home', 6001)
-    resp = sip.login(uid='scclient',
-                     pwd='clientpwd', locn='The basement')
-    pprint(resp)
-    pprint(sip.status())
-
-    pprint(sip.send(PATRON_INFO, PATRON_INFO_RESP,
-                   {'patron':'scclient',
-                    'startitem':1, 'enditem':2}))
-
-    # these are items from openncip's test database.
-    item_ids = ['1565921879', '0440242746', '660']
-    bad_ids = ['xx' + i for i in item_ids]
-    for item in (item_ids + bad_ids):
-        result = sip.send(ITEM_INFORMATION, ITEM_INFO_RESP,
-                          {'item':item})
-        print '%-12s: %s' % (item, result['title'] or '????')
-        print sip.send(CHECKOUT, RAW,
-                       {'patron':'scclient-2',
-                        'inst': 'UWOLS',
-                        'item':item})
-        print '\n' * 5
-    pprint(sip.send(END_PATRON_SESSION, END_SESSION_RESP,
-                   {'patron':'scclient',
-                    'inst':'UWOLS'}))
-
-
+# Small portions are borrowed from David Fiander's acstest.py, in the\r
+# openncip project. David's license is below:\r
+\r
+# Copyright (C) 2006-2008  Georgia Public Library Service\r
+# \r
+# Author: David J. Fiander\r
+# \r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of version 2 of the GNU General Public\r
+# License as published by the Free Software Foundation.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public\r
+# License along with this program; if not, write to the Free\r
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,\r
+# MA 02111-1307 USA\r
+\r
+\r
+from sipconstants import *\r
+import socket\r
+import sys\r
+from datetime import datetime\r
+import re\r
+\r
+DEBUG = True\r
+\r
+# ------------------------------------------------------------\r
+# helper functions\r
+\r
+def split_n(n):\r
+    """Return a function that splits a string into two parts at index N."""\r
+    return lambda s: (s[:n], s[n:])\r
+\r
+split2 = split_n(2)\r
+\r
+\r
+\r
+\r
+# ------------------------------------------------------------\r
+# Messages\r
+\r
+# First we build up a little language for defining SIP messages, so\r
+# that we can define the protocol in a declarative style.\r
+\r
+\r
+class basefield(object): \r
+\r
+    def encode(self, dct):\r
+        """Take a dict, and return the wire representation of this field."""\r
+        raise NotImplementedError, repr(self)\r
+\r
+    def decode(self, bytes):\r
+        """\r
+        Take a wire representation and return a pair (V,R) where V is\r
+        the translated value of the current field, and R is the\r
+        remaining bytes after the field has been read. If this is an\r
+        optional field, then decode should return None for V, and\r
+        return the input bytes for R.\r
+        """\r
+        raise NotImplementedError, repr(self)\r
+\r
+\r
+class field(basefield):\r
+\r
+    def __init__(self, name, code, width=None):\r
+        self.name = name \r
+        self.code = code\r
+        self.width = None       # don't use this yet.\r
+\r
+    def encode(self, dct):\r
+        return '%s%s|' % (self.code, dct.get(self.name, ''))\r
+\r
+    def decode(self, bytes):\r
+        bcode, rest = split2(bytes)\r
+        if bcode != self.code:\r
+            raise 'BadDecode', \\r
+                'Wrong field! Expected %r (%s) got %r (%s), in %r.' % (\r
+                    self.code, lookup_constant(self.code),\r
+                    bcode, lookup_constant(bcode),\r
+                    bytes)\r
+        data, rest = rest.split('|', 1)\r
+        return data, rest\r
+\r
+\r
+class optfield(field):          # an optional field\r
+\r
+    def decode(self, bytes):\r
+        tmp = bytes + '  '\r
+        bcode, rest = split2(tmp)\r
+        if bcode == self.code:\r
+            return field.decode(self, bytes)\r
+        else:\r
+            return None, bytes\r
+\r
+        \r
+class charfield(basefield):\r
+\r
+    def __init__(self, name, width=None, default=None):\r
+        self.name = name\r
+        self.dflt = str(default)\r
+        self.width = width or len(self.dflt) # give at least one\r
+        self.pad = ' ' * self.width\r
+\r
+        self.decode = split_n(self.width)\r
+\r
+    def encode(self, dct):\r
+        v = dct.get(self.name, self.dflt)\r
+        assert v is not None\r
+        return ('%s%s' % (self.pad, v))[-self.width:]\r
+\r
+\r
+class yn(basefield):\r
+    def __init__(self, name):\r
+        self.name = name\r
+\r
+    def encode(self, dct):\r
+        return 'NY'[bool(dct.get(self.name))]\r
+\r
+    def decode(self, bytes):\r
+        return (bytes[0] == 'Y'), bytes[1:]\r
+\r
+\r
+class localtime(charfield):\r
+    def __init__(self, name):\r
+        self.name = name\r
+        self.width = 18\r
+\r
+    def encode(self, dct):\r
+        return datetime.now().strftime('%Y%m%d    %H%M%S')\r
+\r
+    def decode(self, bytes):\r
+        return split_n(self.width)(bytes)\r
+\r
+RAW = -55\r
+class raw(basefield):\r
+    name = 'raw'\r
+    # for debugging.\r
+    def decode(self, bytes):\r
+        return bytes, '\r'\r
+\r
+# We define a protocol Message as a list of fields. For now,\r
+# message(A, B, C) is equivalent to the tuple (A,B,C).\r
+\r
+message = lambda *args: args\r
+\r
+# Encoding a message on to the wire. Args is a dict of field-values.\r
+\r
+def encode_msg(msg, args):\r
+    out = []\r
+    add = out.append\r
+    for thing in msg:\r
+        if isinstance(thing, basefield):\r
+            add(thing.encode(args))\r
+        else:\r
+            add(str(thing))\r
+    return ''.join(out)\r
+\r
+# Decoding from the wire:\r
+\r
+def decode_msg(msg, bytes):\r
+    out = {}\r
+    add = out.__setitem__\r
+    rest = bytes\r
+    \r
+    # Proper 'fields' have variable position in the tail of the\r
+    # message. So we treat them differently.\r
+    varposn = set([p for p in msg if isinstance(p, field)])\r
+    varlookup = dict((x.code, x) for x in varposn)\r
+    fixedposn = [p for p in msg if not p in varposn]\r
+    \r
+    for part in fixedposn:\r
+        if isinstance(part, basefield):\r
+            good, rest = part.decode(rest)\r
+            if good is not None:\r
+                add(part.name, good)\r
+        else:\r
+            v = str(part)\r
+            good, rest = rest[:len(v)], rest[len(v):]\r
+            assert v == good\r
+        if DEBUG: print '%s == %r\n==== %r' % (getattr(part, 'name',''), good, rest)\r
+\r
+    # Now we take what's left, chunk it, and try to resolve each one\r
+    # against a variable-position field.\r
+    segments = re.findall(r'(.*?\|)', rest)\r
+    \r
+    if DEBUG: print segments\r
+\r
+    for segment in segments:\r
+        fld = varlookup.get(segment[:2])\r
+        if fld:\r
+            good, rest = fld.decode(segment)\r
+            add(fld.name, good)\r
+            varposn.remove(fld)\r
+        else:\r
+            raise 'FieldNotProcessed: %s, %s' % (segment, lookup_constant(segment[:2]))\r
+\r
+    # Let's make sure that any "required" fields were not missing.\r
+    notpresent = set(f for f in varposn if not isinstance(f, optfield))\r
+    if notpresent:\r
+        for f in notpresent:\r
+            print 'MISSING: %-12s %s %s' % (f.name, f.code, lookup_constant(f.code))\r
+        raise 'MandatoryFieldsNotPresent'\r
+\r
+    return out\r
+\r
+# The SIP checksum. Borrowed from djfiander.        \r
+\r
+def checksum(msg):\r
+    return '%04X' % ((0 - sum(map(ord, msg))) & 0xFFFF)\r
+\r
+\r
+#------------------------------------------------------------\r
+# SIP Message Definitions\r
+\r
+# some common fields\r
+\r
+\r
+fld_localtime     = localtime('localtime')\r
+fld_INST_ID       = field('inst', FID_INST_ID)\r
+fld_ITEM_ID       = field('item', FID_ITEM_ID)\r
+fld_PATRON_ID     = field('patron', FID_PATRON_ID)\r
+ofld_TERMINAL_PWD = optfield('termpwd', FID_TERMINAL_PWD)\r
+fld_proto_version = charfield('version', default='2.00')\r
+ofld_print_line    = optfield('print_line', FID_PRINT_LINE)\r
+ofld_screen_msg    = optfield('screenmsg', FID_SCREEN_MSG)\r
+\r
+MESSAGES = {\r
+    LOGIN : message(\r
+            LOGIN, \r
+            '00',\r
+            field('uid', FID_LOGIN_UID),\r
+            field('pwd', FID_LOGIN_PWD),\r
+            field('locn', FID_LOCATION_CODE)),\r
+\r
+    LOGIN_RESP : message(\r
+            LOGIN_RESP, \r
+            charfield('ok', width=1)),\r
+\r
+    SC_STATUS : message(\r
+            SC_STATUS, \r
+            charfield('online', default='1'),\r
+            charfield('width', default='040'),\r
+            fld_proto_version),\r
+\r
+    ACS_STATUS : message(\r
+            ACS_STATUS,\r
+            yn('online'),\r
+            yn('checkin_OK'),\r
+            yn('checkout_OK'),\r
+            yn('renewal_OK'),\r
+            yn('status_update_OK'),\r
+            yn('offline_OK'),\r
+            charfield('timeout', default='01'),\r
+            charfield('retries', default='9999'),\r
+            fld_localtime,\r
+            charfield('protocol', default='2.00'),\r
+            fld_INST_ID,\r
+            optfield('patron_id', FID_PATRON_ID),\r
+            optfield('item_id', FID_ITEM_ID),\r
+            optfield('terminal_pwd', FID_TERMINAL_PWD),\r
+            \r
+            optfield('instname', FID_LIBRARY_NAME),\r
+            field('supported', FID_SUPPORTED_MSGS),\r
+            optfield('ttylocn', FID_TERMINAL_LOCN),\r
+            ofld_screen_msg,\r
+            ofld_print_line),\r
+    PATRON_INFO : message(\r
+            PATRON_INFO,\r
+            charfield('lang', width=3, default=1),\r
+            fld_localtime,\r
+            charfield('holditemsreq', default='Y         '),\r
+            fld_INST_ID,\r
+            fld_PATRON_ID,\r
+            ofld_TERMINAL_PWD,\r
+            optfield('patronpwd', FID_PATRON_PWD),\r
+            optfield('startitem', FID_START_ITEM, width=5),\r
+            optfield('enditem', FID_END_ITEM, width=5)),\r
+            \r
+    PATRON_INFO_RESP : message(\r
+            PATRON_INFO_RESP,\r
+            charfield('hmmm', width=14),\r
+            charfield('lang', width=3, default=1),\r
+            fld_localtime,\r
+            charfield('onhold', width=4),\r
+            charfield('overdue', width=4),\r
+            charfield('charged', width=4),\r
+            charfield('fine', width=4),\r
+            charfield('recall', width=4),\r
+            charfield('unavail_holds', width=4),\r
+            fld_INST_ID,\r
+            ofld_screen_msg,\r
+            ofld_print_line,\r
+            optfield('instname', FID_LIBRARY_NAME),\r
+            fld_PATRON_ID,\r
+            field('personal', FID_PERSONAL_NAME),\r
+\r
+            optfield('hold_limit', FID_HOLD_ITEMS_LMT, width=4),\r
+            optfield('overdue_limit', FID_OVERDUE_ITEMS_LMT, width=4),\r
+            optfield('charged_limit', FID_OVERDUE_ITEMS_LMT, width=4),\r
+\r
+            optfield('hold_items', FID_HOLD_ITEMS),\r
+            optfield('valid_patron_pwd', FID_VALID_PATRON_PWD),\r
+            \r
+            optfield('valid_patron', FID_VALID_PATRON),\r
+            optfield('currency', FID_CURRENCY),\r
+            optfield('fee_amt', FID_FEE_AMT),\r
+            optfield('fee_limit', FID_FEE_LMT),\r
+            optfield('home_addr', FID_HOME_ADDR),\r
+            optfield('email', FID_EMAIL),\r
+            optfield('home_phone', FID_HOME_PHONE),\r
+            optfield('patron_birthdate', FID_PATRON_BIRTHDATE),\r
+            optfield('patron_class', FID_PATRON_CLASS),\r
+            optfield('inet_profile', FID_INET_PROFILE),\r
+            optfield('home_library', FID_HOME_LIBRARY)),\r
+\r
+    END_PATRON_SESSION : message(\r
+            END_PATRON_SESSION,\r
+            fld_localtime,\r
+            field('inst', FID_INST_ID),\r
+            field('patron', FID_PATRON_ID)),\r
+\r
+    END_SESSION_RESP : message(\r
+            END_SESSION_RESP,\r
+            yn('session_ended'),\r
+            fld_localtime,\r
+            fld_INST_ID,\r
+            fld_PATRON_ID,\r
+            ofld_print_line,\r
+            ofld_screen_msg),\r
+\r
+    CHECKOUT: message(\r
+        CHECKOUT,\r
+        yn('renewals_OK'),\r
+        yn('no_block'),\r
+        fld_localtime,\r
+        fld_localtime,\r
+        field('inst', FID_INST_ID),\r
+        field('patron', FID_PATRON_ID),\r
+        field('item', FID_ITEM_ID),\r
+        ),\r
+\r
+    CHECKOUT_RESP: message(\r
+        CHECKOUT_RESP,\r
+        charfield('ok', width=1),\r
+        yn('is_renewal'),\r
+        yn('is_magnetic'),\r
+        yn('desensitize'),\r
+        fld_localtime,\r
+        field('inst', FID_INST_ID),\r
+        field('patron', FID_PATRON_ID),\r
+        field('item', FID_ITEM_ID),\r
+        field('due', FID_DUE_DATE),\r
+        field('title', FID_TITLE_ID),\r
+        optfield('media_type_code', FID_MEDIA_TYPE),\r
+        optfield('is_valid_patron', FID_VALID_PATRON),\r
+        ofld_print_line,\r
+        ofld_screen_msg),\r
+\r
+    CHECKIN: message(\r
+        CHECKIN,\r
+        yn('is_retry'),\r
+        fld_localtime,\r
+        fld_localtime,\r
+        field('item', FID_ITEM_ID),\r
+        field('location', FID_CURRENT_LOCN),\r
+        field('inst', FID_INST_ID),\r
+        ofld_TERMINAL_PWD,\r
+        ),\r
+\r
+    CHECKIN_RESP: message(\r
+        CHECKIN_RESP,\r
+        charfield('ok', width=1),\r
+        yn('resensitize'),\r
+        yn('is_magnetic'),\r
+        yn('alert'),\r
+        fld_localtime,\r
+        fld_INST_ID,\r
+        optfield('patron', FID_PATRON_ID),\r
+        field('item', FID_ITEM_ID),\r
+        field('title', FID_TITLE_ID),\r
+        optfield('media_type_code', FID_MEDIA_TYPE),\r
+        optfield('perm_locn', FID_PERM_LOCN),\r
+        optfield('due', FID_DUE_DATE),\r
+        ofld_print_line,\r
+        ofld_screen_msg,\r
+        ),\r
+#         yn('is_retry'),\r
+#         fld_localtime,\r
+#         fld_localtime,\r
+#         field('item', FID_ITEM_ID),\r
+#         field('location', FID_CURRENT_LOCN),\r
+#         ofld_TERMINAL_PWD,\r
+#        ),\r
+\r
+    ITEM_INFORMATION : message(\r
+            ITEM_INFORMATION,\r
+            fld_localtime,\r
+            fld_INST_ID,\r
+            fld_ITEM_ID,\r
+            ofld_TERMINAL_PWD),\r
+\r
+    ITEM_INFO_RESP : message(\r
+            ITEM_INFO_RESP,\r
+            charfield('circstat', width=2),\r
+            charfield('security', width=2),\r
+            charfield('feetype', width=2),\r
+            fld_localtime,\r
+            fld_ITEM_ID,\r
+            field('title', FID_TITLE_ID),\r
+            optfield('mediatype', FID_MEDIA_TYPE),\r
+            optfield('perm_locn', FID_PERM_LOCN),\r
+            optfield('current_locn', FID_CURRENT_LOCN),\r
+            optfield('item_props', FID_ITEM_PROPS),\r
+            optfield('currency', FID_CURRENCY),\r
+            optfield('fee', FID_FEE_AMT),\r
+            optfield('owner', FID_OWNER),\r
+            optfield('hold_queue_len', FID_HOLD_QUEUE_LEN),\r
+            optfield('due_date', FID_DUE_DATE),\r
+\r
+            optfield('recall_date', FID_RECALL_DATE),\r
+            optfield('hold_pickup_date', FID_HOLD_PICKUP_DATE),\r
+            ofld_screen_msg,\r
+            ofld_print_line),\r
+            \r
+    RAW : message(raw()),\r
+}\r
+\r
+\r
+class SipClient(object):\r
+    def __init__(self, host, port, error_detect=False):\r
+        self.hostport = (host, port)\r
+        self.error_detect = error_detect\r
+        self.connect()\r
+\r
+    def connect(self):\r
+        so = socket.socket()\r
+        so.connect(self.hostport)\r
+        self.socket = so\r
+        self.seqno = self.error_detect and 1 or 0\r
+\r
+    def close(self):\r
+        # fixme, do SIP close first.\r
+        self.socket.close()\r
+\r
+    def send(self, outmsg, inmsg, args=None):\r
+        msg_template = MESSAGES[outmsg]\r
+        resp_template = MESSAGES[inmsg]\r
+        msg = encode_msg(msg_template, args or {})\r
+        if self.error_detect:\r
+            # add the checksum\r
+            msg += 'AY%dAZ' % (self.seqno % 10)\r
+            self.seqno += 1\r
+            msg += checksum(msg)\r
+        msg += '\r'\r
+        if DEBUG: print '>>> %r' % msg\r
+        self.socket.send(msg)\r
+        resp = self.socket.recv(1000)\r
+        if DEBUG: print '<<< %r' % resp\r
+        return decode_msg(resp_template, resp)\r
+        \r
+\r
+    # --------------------------------------------------\r
+    # Common protocol methods\r
+\r
+    def login(self, uid, pwd, locn):\r
+        msg = self.send(LOGIN, LOGIN_RESP, \r
+                        dict(uid=uid, pwd=pwd, locn=locn))\r
+        return msg.get('ok') == '1'\r
+\r
+    def status(self):\r
+        return self.send(SC_STATUS, ACS_STATUS)\r
+\r
+    def patron_info(self, barcode):\r
+        msg = self.send(PATRON_INFO,PATRON_INFO_RESP,\r
+                        {'patron':barcode,\r
+                         'startitem':1, 'enditem':2})\r
+        # fixme, this may not be the best test of okayness\r
+        msg['success'] = msg.get('valid_patron') == 'Y'\r
+        return msg\r
+\r
+    def checkout(self, patron, item, inst=''):\r
+        msg = self.send(CHECKOUT, CHECKOUT_RESP,\r
+                        {'patron':patron,\r
+                         'inst': inst,\r
+                         'item':item})\r
+        msg['media_type'] = MEDIA_TYPE_TABLE.get(msg.get('media_type_code'))\r
+        msg['success'] = msg.get('ok') == '1'\r
+        return msg\r
+\r
+    def checkin(self, item, institution='', location=''):\r
+        msg = self.send(CHECKIN, CHECKIN_RESP,\r
+                        {'inst': institution,\r
+                         'location':location,\r
+                         'is_retry':False,\r
+                         'item':item})\r
+        msg['success'] = msg.get('ok') == '1'\r
+        return msg\r
+\r
+    def item_info(self, barcode):\r
+        print("starting")\r
+        msg = self.send(ITEM_INFORMATION, ITEM_INFO_RESP,\r
+                        {'item':barcode})\r
+        print(msg['circstat'])\r
+        msg['available'] = msg['circstat'] == '03'\r
+        msg['status'] = ITEM_STATUS_TABLE[msg['circstat']]\r
+        return msg\r
+\r
+\r
+# ------------------------------------------------------------\r
+# Django stuff. Optional.\r
+\r
+try:\r
+    from django.conf import settings\r
+    def sip_connection():\r
+        sip = SipClient(*settings.SIP_HOST)\r
+        if not sip.login(*settings.SIP_CREDENTIALS):\r
+            raise 'SipLoginError'\r
+        return sip\r
+\r
+    # decorator\r
+    def SIP(fn):\r
+        def f(*args, **kwargs):\r
+            conn = sip_connection()\r
+            resp = fn(conn, *args, **kwargs)\r
+            conn.close()\r
+            return resp\r
+        return f\r
+\r
+except ImportError:\r
+    pass\r
+\r
+\r
+# ------------------------------------------------------------\r
+# Test code.\r
+\r
+if __name__ == '__main__':\r
+    from pprint import pprint\r
+\r
+    sip = SipClient('comet.cs.uoguelph.ca', 8080)\r
+    resp = sip.login(uid='test',\r
+                     pwd='test', locn='test')\r
+    pprint(resp)\r
+    pprint(sip.status())\r
+\r
+    pprint(sip.send(PATRON_INFO, PATRON_INFO_RESP,\r
+                   {'patron':'scclient',\r
+                    'startitem':1, 'enditem':2}))\r
+\r
+    # these are items from openncip's test database.\r
+    item_ids = ['1565921879', '0440242746', '660']\r
+    bad_ids = ['xx' + i for i in item_ids]\r
+    for item in (item_ids + bad_ids):\r
+        result = sip.send(ITEM_INFORMATION, ITEM_INFO_RESP,\r
+                          {'item':item})\r
+        print '%-12s: %s' % (item, result['title'] or '????')\r
+        print sip.send(CHECKOUT, RAW,\r
+                       {'patron':'scclient-2',\r
+                        'inst': 'UWOLS',\r
+                        'item':item})\r
+        print '\n' * 5\r
+    pprint(sip.send(END_PATRON_SESSION, END_SESSION_RESP,\r
+                   {'patron':'scclient',\r
+                    'inst':'UWOLS'}))\r
+\r
+\r
index 7c8ae6a..7682924 100644 (file)
@@ -106,13 +106,13 @@ AUTHENTICATION_BACKENDS = (
 \r
 EVERGREEN_GATEWAY_SERVER = 'www.concat.ca'\r
 Z3950_CONFIG = ('zed.concat.ca', 210, 'OWA')  #OWA,OSUL,CONIFER\r
-SIP_HOST = ('comet.cs.uoguelph.ca', 8080)\r
+SIP_HOST = ('localhost', 8080)\r
 \r
 try:\r
     from private_local_settings import SIP_CREDENTIALS\r
 except:\r
     # stuff that I really ought not check into svn...\r
-    #SIP_CREDENTIALS = ('userid', 'password', 'location')\r
+    SIP_CREDENTIALS = ('test', 'test', 'test')\r
     pass\r
 \r
 \r
index 735db0f..6306da2 100644 (file)
-html, body, div, span, applet, object, iframe,
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, font, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td {
-       margin: 0;
-       padding: 0;
-       border: 0;
-       outline: 0;
-       font-weight: inherit;
-       font-style: inherit;
-       font-size: 100%;
-       font-family: inherit;
-       vertical-align: baseline;
-}
-/* remember to define focus styles! */
-:focus {
-       outline: 0;
-}
-body {
-       line-height: 1;
-       color: black;
-       background: white;
-}
-ol, ul {
-       list-style: none;
-}
-/* tables still need 'cellspacing="0"' in the markup */
-table {
-       border-collapse: separate;
-       border-spacing: 0;
-}
-caption, th, td {
-       text-align: left;
-       font-weight: normal;
-}
-blockquote:before, blockquote:after,
-q:before, q:after {
-       content: "";
-}
-blockquote, q {
-       quotes: "" "";
-}
-
-/* General look and feel */
-
-body * {  font-family: Verdana, sans-serif; }
-pre, code { font-family: monospace; }
-
-body, html { margin: 0; padding: 0; }
-
-body {  
-    background-color: #005; 
-    font-size: normal; 
-}
-
-div#outer {
-    background-color: white; 
-    width: 960px; margin-bottom: 50px;
-    margin-left: auto; margin-right: auto;
-}
-#mainpanel {  background-color: white; padding: 0 12px 24px 12px; 
-min-height: 300px; 
-}
-
-/* General headers and footers */
-
-#header, #footer {
-    color: white; padding: 8px 4px 12px 8px;
-    background-color: #448;
-}
-
-#header div#search {
-    float: right;
-    margin: 0;
-    color: #ccc;
-}
-
-#header #welcome {
-    margin-top: 4px;
-}
-
-#brandheader { background-color: white; padding: 8px; }
-
-#header a { color: #fff; font-weight: bold;  padding: 10px 12px 10px 12px; }
-#header a.loginbutton { background-color: #a44; }
-#header a:hover { background-color: #fb7; color: black; text-decoration: none; }
-
-tbody td, tbody th { vertical-align: top; }
-
-#footer {  
-    margin: 12px;
-    padding-bottom: 12px;
-    background-color: #ddf; 
-    color: black;
-    border-bottom: white 10px solid;
-}
-
-/* heading sizes and colours. */
-
-h1 { font-size: 150%; font-weight: bold; }
-h2 { font-size: 125%; font-weight: bold; }
-h3 { font-size: 120%; font-weight: bold; }
-p { margin: 24px 0px; }
-h1 { color: navy; margin: 36px 0 18px 0; }
-h2 { color: #336; margin: 24px 0 12px 0; }
-h3, h4 { color: darkgreen; }
-h1 a, h2 a { color: navy; }
-
-a { color: blue; text-decoration: none; }
-a:hover {  text-decoration: underline;  }
-
-/* error panel on the person-edit form */
-
-.errors {  margin: 1em; padding: 1em; background-color: #fdd; }
-.errors h2 { font-size: 120%; }
-
-/* actions (e.g. "edit user" link) */
-.action a { font-weight: bold; }
-
-#tabbar { margin: 18px 0; padding: 0; clear: both; }
-#tabbar li { display: inline; }
-#tabbar li a { padding: 15px 18px 5px 18px; background-color: #ddf; color: black; text-decoration: none; }
-#tabbar li a:hover { background-color: #fc8; }
-
-/* 
-#tabbar li.active a { background-color: #fa6; font-weight: bold; }
-*/
-
-.pagination_controls {
-    text-align: center; margin: 12px 0;
-}
-
-.pagination_controls .nums {
-    padding: 0 12px; 
-}
-
-.pagetable td { border: #ddd 1px solid; padding: 8px; }
-.pagetable .odd {
-    background-color: #F8F8F8;
-}
-.pagetable thead th { font-size: smaller; text-align: left; padding: 2px 8px; }
-
-
-/* nested titles: like breadcrumbs when drilling into an itemtree */
-
-.nestedtitle h2 { margin-top: 8px; }
-.nestedtitle a { color: navy; }
-
-span.final_item { font-weight: bold; font-size: 110%; }
-
-/* item trees (tree of headings and items in a course */
-
-#sidepanel { width: 183px; float: right; text-align: right;}
-#sidepanel div { margin: 6px 0; }
-
-#treepanel { width: 740px; }
-
-.itemtree { 
-    margin-left: 20px;
-    padding-left: 20px;
-    list-style-type: none; 
-}
-
-.itemtree .itemtree { margin-left: 30px; padding-left: 0; }
-
-.itemtree li { padding-left: 0; margin-left: 0; } 
-
-.itemtree li { margin: 12px 8px; }
-.itemtree li .mainline { padding-left: 8px; }
-
-.itemtree .metalink { padding-left: 8px; color: gray; }
-.itemtree .metalink a {
-    color: gray; 
-}
-
-.itemtree .editlinks   { padding-left: 12px; color: gray; 
-                        font-size: small;
-                    }
-.itemtree .editlinks a { color: navy; }
-
-.itemadd { 
-    margin-top: 30px; font-size: 90%;
-    padding: 10px; 
-    background-color: #eef; 
-    clear: both;
-}
-.itemadd li {     
-    margin: 10px; 
-}
-
-.itemadd a { color: navy; }
-
-/* specialized display of items in tree, by type */
-
-.itemtree li.item_HEADING { 
-    list-style-image: url(tango/folder.png);
-}
-.itemtree li.item_HEADING > a { 
-    color: navy; 
-}
-
-li.item_HEADING .headingmainline {
-    margin-bottom: 12px;
-}
-
-li.item_HEADING .headingmainline  a.mainlink {
-    border-bottom: #aaa 1px solid; 
-}
-
-li.item_HEADING  .headingmainline a.mainlink:hover {
-    border-bottom: none;
-}
-
-.itemtree li.item_ELEC { 
-    list-style-image: url(tango/document.png);
-}
-
-.itemtree li.item_URL { 
-    list-style-image: url(tango/applications-internet.png);
-}
-
-.itemtree li.item_PHYS { 
-    /* fixme: need a better icon */
-    list-style-image: url(tango/x-office-address-book.png);
-}
-
-
-.instructors {
-  border: 1px solid #ccc;
-  float: left;
-  width: 50%;
-  padding: 2px 2px 2px 2px;
-  font-size: 1em;
-  line-height: 1em;
-  text-align: left;
-  margin-right: 5px;
-}
-
-.topbox {
-  border: 1px solid #ccc;
-  width: 50%;
-  line-height: 1em;
-  text-align: left;
-}
-
-table.topheading { width: 100%; }
-.topheading th {
-    background-color: #ddf;
-}
-
-.topheading th, .topheading td {
-padding: 8px;
-}
-
-p.todo, div.todo { background-color: #fdd; padding: 6px; margin: 12px; border-left: #d99 6px solid; }
-
-.newsitem p { margin: 12px 0; }
-.newsitem ul { list-style: circle; margin-left: 30px; }
-.newsitem ul li { margin: 8px; }
-
-.newsitem { 
-    max-width: 600px;
-    line-height: 125%;
-}
-.newsitem .newsdate { 
-    margin: 4px 0 8px 0; text-align: right; 
-    font-size: 80%; color: navy;
-}
-
-.menublockopener { margin-left: 0.25em; color: #bbb !important; font-weight: normal !important; }
-.menublock { background-color: #f2e4cc; font-size: 95%; padding: 1px 4px; }
-
-#coursebanner { background-color: #f2e4cc; margin: -12px -12px 12px -12px; padding: 8px; }
-#coursesearch { float: right; }
-#coursebanner h1 { margin: 12px 0; font-size: 125%; }
-
-#edit_course_link { margin: 8px 0 8px 0; font-size: 95%; }
-
-
-#breadcrumbs { margin: 8px 0px 16px 0; width: 716px;  
-            text-indent: -24px; padding-left: 24px; }
-
-.errorlist { float: right; }
-.errorlist li { color: red; font-size: 90%; }
-
-
-/* a nice table-style for forms. */
-.formtable tbody th { 
-    padding: 0 8px 16px 0;
-    width: 200px; 
-    text-align: left; 
-    font-size: 90%;
-    font-weight: normal; 
-}
-
-thead th { padding: 8px; font-weight: bold; font-size: 90%; }
-.metadata_table tbody th,
-.metadata_table tbody td {
-    padding: 8px; border: #ddd 1px solid;
-}
-.metadata_table input { width: 600px; }
-.metadata_table .meta3 input { width: 10px; }
-
-.metadata_table tbody th {
-    background-color: #eee;
-}
-   
-.metadata_table a.bigdownload { padding: 8px 58px; font-weight: bold; font-size: 105%; }
-.metadata_table a.bigdownload:hover { background-color: #dfd; color: black; }
-
-h2.metadata_subhead {font-size: 105%; padding: 0; margin: 18px 0 9px 0;}
-
-.metadata_table tbody th {
-    text-align: left; width: 120px;
-}
-.gap { height: 24px; }
-.metadata_table td { max-width: 800px; overflow: hidden; }
-
-/* panels that appear when specific OPTIONs or radio-buttons are selected. */
-.specific { padding: 8px; margin: 0 16px; background-color: #eef; }
-
-
-li.sort_item { margin-top: 20px !important;
-            border: gray 1px dotted; width: 400px; }
-
-li.sort_item:hover { background-color: #eee; }
-
-ul.heading_tree li  { list-style: none; }
-ul.heading_tree { margin: 0; padding-left: 0; }
-ul.heading_tree ul { margin: 0; padding-left: 25px; }
-
+html, body, div, span, applet, object, iframe,\r
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,\r
+a, abbr, acronym, address, big, cite, code,\r
+del, dfn, em, font, img, ins, kbd, q, s, samp,\r
+small, strike, strong, sub, sup, tt, var,\r
+dl, dt, dd, ol, ul, li,\r
+fieldset, form, label, legend,\r
+table, caption, tbody, tfoot, thead, tr, th, td {\r
+       margin: 0;\r
+       padding: 0;\r
+       border: 0;\r
+       outline: 0;\r
+       font-weight: inherit;\r
+       font-style: inherit;\r
+       font-size: 100%;\r
+       font-family: inherit;\r
+       vertical-align: baseline;\r
+}\r
+/* remember to define focus styles! */\r
+:focus {\r
+       outline: 0;\r
+}\r
+body {\r
+       line-height: 1;\r
+       color: black;\r
+       background: white;\r
+}\r
+ol, ul {\r
+       list-style: none;\r
+}\r
+/* tables still need 'cellspacing="0"' in the markup */\r
+table {\r
+       border-collapse: separate;\r
+       border-spacing: 0;\r
+}\r
+caption, th, td {\r
+       text-align: left;\r
+       font-weight: normal;\r
+}\r
+blockquote:before, blockquote:after,\r
+q:before, q:after {\r
+       content: "";\r
+}\r
+blockquote, q {\r
+       quotes: "" "";\r
+}\r
+\r
+/* General look and feel */\r
+\r
+body * {  font-family: Verdana, sans-serif; }\r
+pre, code { font-family: monospace; }\r
+\r
+body, html { margin: 0; padding: 0; }\r
+\r
+body {  \r
+    background-color: #005; \r
+    font-size: normal; \r
+}\r
+\r
+div#outer {\r
+    background-color: white; \r
+    width: 960px; margin-bottom: 50px;\r
+    margin-left: auto; margin-right: auto;\r
+}\r
+#mainpanel {  background-color: white; padding: 0 12px 24px 12px; \r
+min-height: 300px; \r
+}\r
+\r
+/* General headers and footers */\r
+\r
+#header, #footer {\r
+    color: white; padding: 8px 4px 12px 8px;\r
+    background-color: #448;\r
+}\r
+\r
+#header div#search {\r
+    float: right;\r
+    margin: 0;\r
+    color: #ccc;\r
+}\r
+\r
+#header #welcome {\r
+    margin-top: 4px;\r
+}\r
+\r
+#brandheader { background-color: white; padding: 8px; }\r
+\r
+#header a { color: #fff; font-weight: bold;  padding: 10px 12px 10px 12px; }\r
+#header a.loginbutton { background-color: #a44; }\r
+#header a:hover { background-color: #fb7; color: black; text-decoration: none; }\r
+\r
+tbody td, tbody th { vertical-align: top; }\r
+\r
+#footer {  \r
+    margin: 12px;\r
+    padding-bottom: 12px;\r
+    background-color: #ddf; \r
+    color: black;\r
+    border-bottom: white 10px solid;\r
+}\r
+\r
+/* heading sizes and colours. */\r
+\r
+h1 { font-size: 150%; font-weight: bold; }\r
+h2 { font-size: 125%; font-weight: bold; }\r
+h3 { font-size: 120%; font-weight: bold; }\r
+p { margin: 24px 0px; }\r
+h1 { color: navy; margin: 36px 0 18px 0; }\r
+h2 { color: #336; margin: 24px 0 12px 0; }\r
+h3, h4 { color: darkgreen; }\r
+h1 a, h2 a { color: navy; }\r
+\r
+a { color: blue; text-decoration: none; }\r
+a:hover {  text-decoration: underline;  }\r
+\r
+/* error panel on the person-edit form */\r
+\r
+.errors {  margin: 1em; padding: 1em; background-color: #fdd; }\r
+.errors h2 { font-size: 120%; }\r
+\r
+/* actions (e.g. "edit user" link) */\r
+.action a { font-weight: bold; }\r
+\r
+#tabbar { margin: 18px 0; padding: 0; clear: both; }\r
+#tabbar li { display: inline; }\r
+#tabbar li a { padding: 15px 18px 5px 18px; background-color: #ddf; color: black; text-decoration: none; }\r
+#tabbar li a:hover { background-color: #fc8; }\r
+\r
+/* \r
+#tabbar li.active a { background-color: #fa6; font-weight: bold; }\r
+*/\r
+\r
+.pagination_controls {\r
+    text-align: center; margin: 12px 0;\r
+}\r
+\r
+.pagination_controls .nums {\r
+    padding: 0 12px; \r
+}\r
+\r
+.pagetable td { border: #ddd 1px solid; padding: 8px; }\r
+.pagetable .odd {\r
+    background-color: #F8F8F8;\r
+}\r
+.pagetable thead th { font-size: smaller; text-align: left; padding: 2px 8px; }\r
+\r
+\r
+/* nested titles: like breadcrumbs when drilling into an itemtree */\r
+\r
+.nestedtitle h2 { margin-top: 8px; }\r
+.nestedtitle a { color: navy; }\r
+\r
+span.final_item { font-weight: bold; font-size: 110%; }\r
+\r
+/* item trees (tree of headings and items in a course */\r
+\r
+#sidepanel { width: 183px; float: right; text-align: right;}\r
+#sidepanel div { margin: 6px 0; }\r
+\r
+#treepanel { width: 740px; }\r
+\r
+.helptext { \r
+    margin-top: 30px; font-size: 90%;\r
+    padding: 10px; \r
+    background-color: #eef; \r
+    clear: both;\r
+}\r
+\r
+.itemtree { \r
+    margin-left: 20px;\r
+    padding-left: 20px;\r
+    list-style-type: none; \r
+}\r
+\r
+.itemtree .itemtree { margin-left: 30px; padding-left: 0; }\r
+\r
+.itemtree li { padding-left: 0; margin-left: 0; } \r
+\r
+.itemtree li { margin: 12px 8px; }\r
+.itemtree li .mainline { padding-left: 8px; }\r
+\r
+.itemtree .metalink { padding-left: 8px; color: gray; }\r
+.itemtree .metalink a {\r
+    color: gray; \r
+}\r
+\r
+.itemtree .editlinks   { padding-left: 12px; color: gray; \r
+                        font-size: small;\r
+                    }\r
+.itemtree .editlinks a { color: navy; }\r
+\r
+.itemadd { \r
+    margin-top: 30px; font-size: 90%;\r
+    padding: 10px; \r
+    background-color: #eef; \r
+    clear: both;\r
+}\r
+.itemadd li {     \r
+    margin: 10px; \r
+}\r
+\r
+.itemadd a { color: navy; }\r
+\r
+/* specialized display of items in tree, by type */\r
+\r
+.itemtree li.item_HEADING { \r
+    list-style-image: url(tango/folder.png);\r
+}\r
+.itemtree li.item_HEADING > a { \r
+    color: navy; \r
+}\r
+\r
+li.item_HEADING .headingmainline {\r
+    margin-bottom: 12px;\r
+}\r
+\r
+li.item_HEADING .headingmainline  a.mainlink {\r
+    border-bottom: #aaa 1px solid; \r
+}\r
+\r
+li.item_HEADING  .headingmainline a.mainlink:hover {\r
+    border-bottom: none;\r
+}\r
+\r
+.itemtree li.item_ELEC { \r
+    list-style-image: url(tango/document.png);\r
+}\r
+\r
+.itemtree li.item_URL { \r
+    list-style-image: url(tango/applications-internet.png);\r
+}\r
+\r
+.itemtree li.item_PHYS { \r
+    /* fixme: need a better icon */\r
+    list-style-image: url(tango/x-office-address-book.png);\r
+}\r
+\r
+\r
+.instructors {\r
+  border: 1px solid #ccc;\r
+  float: left;\r
+  width: 50%;\r
+  padding: 2px 2px 2px 2px;\r
+  font-size: 1em;\r
+  line-height: 1em;\r
+  text-align: left;\r
+  margin-right: 5px;\r
+}\r
+\r
+.topbox {\r
+  border: 1px solid #ccc;\r
+  width: 50%;\r
+  line-height: 1em;\r
+  text-align: left;\r
+}\r
+\r
+table.topheading { width: 100%; }\r
+.topheading th {\r
+    background-color: #ddf;\r
+}\r
+\r
+.topheading th, .topheading td {\r
+padding: 8px;\r
+}\r
+\r
+p.todo, div.todo { background-color: #fdd; padding: 6px; margin: 12px; border-left: #d99 6px solid; }\r
+\r
+.newsitem p { margin: 12px 0; }\r
+.newsitem ul { list-style: circle; margin-left: 30px; }\r
+.newsitem ul li { margin: 8px; }\r
+\r
+.newsitem { \r
+    max-width: 600px;\r
+    line-height: 125%;\r
+}\r
+.newsitem .newsdate { \r
+    margin: 4px 0 8px 0; text-align: right; \r
+    font-size: 80%; color: navy;\r
+}\r
+\r
+.menublockopener { margin-left: 0.25em; color: #bbb !important; font-weight: normal !important; }\r
+.menublock { background-color: #f2e4cc; font-size: 95%; padding: 1px 4px; }\r
+\r
+#coursebanner { background-color: #f2e4cc; margin: -12px -12px 12px -12px; padding: 8px; }\r
+#coursesearch { float: right; }\r
+#coursebanner h1 { margin: 12px 0; font-size: 125%; }\r
+\r
+#edit_course_link { margin: 8px 0 8px 0; font-size: 95%; }\r
+\r
+\r
+#breadcrumbs { margin: 8px 0px 16px 0; width: 716px;  \r
+            text-indent: -24px; padding-left: 24px; }\r
+\r
+.errorlist { float: right; }\r
+.errorlist li { color: red; font-size: 90%; }\r
+\r
+\r
+/* a nice table-style for forms. */\r
+.formtable tbody th { \r
+    padding: 0 8px 16px 0;\r
+    width: 200px; \r
+    text-align: left; \r
+    font-size: 90%;\r
+    font-weight: normal; \r
+}\r
+\r
+thead th { padding: 8px; font-weight: bold; font-size: 90%; }\r
+.metadata_table tbody th,\r
+.metadata_table tbody td {\r
+    padding: 8px; border: #ddd 1px solid;\r
+}\r
+.metadata_table input { width: 600px; }\r
+.metadata_table .meta3 input { width: 10px; }\r
+\r
+.metadata_table tbody th {\r
+    background-color: #eee;\r
+}\r
+   \r
+.metadata_table a.bigdownload { padding: 8px 58px; font-weight: bold; font-size: 105%; }\r
+.metadata_table a.bigdownload:hover { background-color: #dfd; color: black; }\r
+\r
+h2.metadata_subhead {font-size: 105%; padding: 0; margin: 18px 0 9px 0;}\r
+\r
+.metadata_table tbody th {\r
+    text-align: left; width: 120px;\r
+}\r
+.gap { height: 24px; }\r
+.metadata_table td { max-width: 800px; overflow: hidden; }\r
+\r
+/* panels that appear when specific OPTIONs or radio-buttons are selected. */\r
+.specific { padding: 8px; margin: 0 16px; background-color: #eef; }\r
+\r
+\r
+li.sort_item { margin-top: 20px !important;\r
+            border: gray 1px dotted; width: 400px; }\r
+\r
+li.sort_item:hover { background-color: #eee; }\r
+\r
+ul.heading_tree li  { list-style: none; }\r
+ul.heading_tree { margin: 0; padding-left: 0; }\r
+ul.heading_tree ul { margin: 0; padding-left: 25px; }\r
+\r
index 755260c..90061e7 100644 (file)
@@ -2,6 +2,7 @@
 from django.utils.simplejson import dumps\r
 from conifer.libsystems.z3950.marcxml import marcxml_dictionary_to_dc as to_dublin\r
 title = _('Add physical or electronic item, by catalogue search')\r
+helptext = _('Use keywords or CCL syntax for searching, for example: ti="detroit river" and au="wilgus"')\r
 dc_keys = ['dc:title', 'dc:creator', 'dc:publisher', 'dc:date']\r
 ?>\r
 <html xmlns="http://www.w3.org/1999/xhtml"\r
@@ -24,6 +25,10 @@ dc_keys = ['dc:title', 'dc:creator', 'dc:publisher', 'dc:date']
     ${course_banner(course)}\r
     ${nested_title(parent_item)}\r
     <h2>${title}</h2>\r
+    <div class="helptext">\r
+    ${helptext}\r
+    </div>\r
+\r
     <form method="GET" action=".">\r
       <input type="text" id="query" name="query" value="${query}" \r
             style="font-size: larger; width: 600px;"/>\r