From: artunit Date: Mon, 17 Aug 2009 14:23:07 +0000 (+0000) Subject: some help text and css changes X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=552171f7d8c467bc574a85626bed8f614d7c446b;p=Syrup.git some help text and css changes git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@621 6d9bc8c9-1ec2-4278-b937-99fde70a366f --- diff --git a/conifer/libsystems/sip/sipclient.py b/conifer/libsystems/sip/sipclient.py index ee887f1..4bce1f8 100644 --- a/conifer/libsystems/sip/sipclient.py +++ b/conifer/libsystems/sip/sipclient.py @@ -1,567 +1,569 @@ -# 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 +# 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): + print("starting") + msg = self.send(ITEM_INFORMATION, ITEM_INFO_RESP, + {'item':barcode}) + print(msg['circstat']) + 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('comet.cs.uoguelph.ca', 8080) + resp = sip.login(uid='test', + pwd='test', locn='test') + 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'})) + + diff --git a/conifer/settings.py b/conifer/settings.py index 7c8ae6a..7682924 100644 --- a/conifer/settings.py +++ b/conifer/settings.py @@ -106,13 +106,13 @@ AUTHENTICATION_BACKENDS = ( EVERGREEN_GATEWAY_SERVER = 'www.concat.ca' Z3950_CONFIG = ('zed.concat.ca', 210, 'OWA') #OWA,OSUL,CONIFER -SIP_HOST = ('comet.cs.uoguelph.ca', 8080) +SIP_HOST = ('localhost', 8080) try: from private_local_settings import SIP_CREDENTIALS except: # stuff that I really ought not check into svn... - #SIP_CREDENTIALS = ('userid', 'password', 'location') + SIP_CREDENTIALS = ('test', 'test', 'test') pass diff --git a/conifer/static/main.css b/conifer/static/main.css index 735db0f..6306da2 100644 --- a/conifer/static/main.css +++ b/conifer/static/main.css @@ -1,335 +1,342 @@ -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, +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; } + +.helptext { + margin-top: 30px; font-size: 90%; + padding: 10px; + background-color: #eef; + clear: both; +} + +.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; } + diff --git a/conifer/templates/item/item_add_cat_search.xhtml b/conifer/templates/item/item_add_cat_search.xhtml index 755260c..90061e7 100644 --- a/conifer/templates/item/item_add_cat_search.xhtml +++ b/conifer/templates/item/item_add_cat_search.xhtml @@ -2,6 +2,7 @@ from django.utils.simplejson import dumps from conifer.libsystems.z3950.marcxml import marcxml_dictionary_to_dc as to_dublin title = _('Add physical or electronic item, by catalogue search') +helptext = _('Use keywords or CCL syntax for searching, for example: ti="detroit river" and au="wilgus"') dc_keys = ['dc:title', 'dc:creator', 'dc:publisher', 'dc:date'] ?> ${title} +
+ ${helptext} +
+