-# 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
-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