[open-ils-commits] r1303 - in servres/trunk/conifer: . integration plumbing syrup (gfawcett)

svn at svn.open-ils.org svn at svn.open-ils.org
Sat Apr 2 20:38:03 EDT 2011


Author: gfawcett
Date: 2011-04-02 20:38:02 -0400 (Sat, 02 Apr 2011)
New Revision: 1303

Added:
   servres/trunk/conifer/integration/evergreen_site.py
Modified:
   servres/trunk/conifer/TODO
   servres/trunk/conifer/integration/uwindsor.py
   servres/trunk/conifer/local_settings.py.example
   servres/trunk/conifer/plumbing/hooksystem.py
   servres/trunk/conifer/syrup/integration.py
   servres/trunk/conifer/syrup/models.py
Log:
Now using classes, not modules, to implement local integrations.

This is a rather big change, structurally, that lets us define integrations
naturally in terms of other integrations. For example, UWindsor is now a
subclass of EvergreenSite; the code which is Windsor-specific isn't mingled
with the general Evergreen stuff.

There are still many Leddy/Windsor-isms (and Evergreen-isms) throughout the
codebase that need to be sorted out into new integration functions.

Modified: servres/trunk/conifer/TODO
===================================================================
--- servres/trunk/conifer/TODO	2011-04-03 00:37:58 UTC (rev 1302)
+++ servres/trunk/conifer/TODO	2011-04-03 00:38:02 UTC (rev 1303)
@@ -1,5 +1,9 @@
 NEW:
 
+* get rid of RESERVES_DESK_NAME from integration/local_settings.
+
+* Syrup used to work with no integration module. Does it still?
+
 * finish i18n and french translation.
 
 * fix evergreen authentication problem ('django.py')

Added: servres/trunk/conifer/integration/evergreen_site.py
===================================================================
--- servres/trunk/conifer/integration/evergreen_site.py	                        (rev 0)
+++ servres/trunk/conifer/integration/evergreen_site.py	2011-04-03 00:38:02 UTC (rev 1303)
@@ -0,0 +1,426 @@
+# See conifer/syrup/integration.py for documentation.
+
+from conifer.libsystems                   import marcxml as M
+from conifer.libsystems.evergreen         import item_status as I
+from conifer.libsystems.evergreen.support import initialize, E1
+from conifer.libsystems.z3950             import pyz3950_search as PZ
+from django.conf                          import settings
+from memoization                          import memoize
+from xml.etree                            import ElementTree as ET
+import re
+import time
+import traceback
+
+OPENSRF_AUTHENTICATE      = "open-ils.auth.authenticate.complete"
+OPENSRF_AUTHENTICATE_INIT = "open-ils.auth.authenticate.init"
+OPENSRF_BATCH_UPDATE      = "open-ils.cat.asset.copy.fleshed.batch.update"
+OPENSRF_CIRC_UPDATE       = "open-ils.cstore open-ils.cstore.direct.action.circulation.update"
+OPENSRF_CLEANUP           = "open-ils.auth.session.delete"
+OPENSRF_CN_BARCODE        = "open-ils.circ.copy_details.retrieve.barcode.authoritative"
+OPENSRF_CN_CALL           = "open-ils.search.asset.copy.retrieve_by_cn_label"
+OPENSRF_COPY_COUNTS       = "open-ils.search.biblio.copy_counts.location.summary.retrieve"
+OPENSRF_FLESHED2_CALL     = "open-ils.search.asset.copy.fleshed2.retrieve"
+OPENSRF_FLESHEDCOPY_CALL  = "open-ils.search.asset.copy.fleshed.batch.retrieve.authoritative"
+
+
+# @disable is used to point out integration methods you might want to define
+# in your subclass, but which are not defined in the basic Evergreen
+# integration.
+
+def disable(func):
+    return None
+
+class EvergreenIntegration(object):
+
+    EG_BASE = 'http://%s/' % settings.EVERGREEN_GATEWAY_SERVER
+    initialize(EG_BASE)
+
+    # USE_Z3950: if True, use Z39.50 for catalogue search; if False, use OpenSRF.
+    # Don't set this value directly here: rather, if there is a valid Z3950_CONFIG
+    # settings in local_settings.py, then Z39.50 will be used.
+
+    USE_Z3950 = getattr(settings, 'Z3950_CONFIG', None) is not None
+
+    TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
+    DUE_FORMAT  = "%b %d %Y, %r"
+
+    # regular expression to detect DVD, CD, CD-ROM, Guide, Booklet on the end of a
+    # call number
+    IS_ATTACHMENT = re.compile('\w*DVD\s?|\w*CD\s?|\w[Gg]uide\s?|\w[Bb]ooklet\s?|\w*CD\-ROM\s?')
+
+    # Item status stuff
+
+    _STATUS_DECODE = [(str(x['id']), x['name'])
+                     for x in E1('open-ils.search.config.copy_status.retrieve.all')]
+
+    AVAILABLE  = [id for id, name in _STATUS_DECODE if name == 'Available'][0]
+    RESHELVING = [id for id, name in _STATUS_DECODE if name == 'Reshelving'][0]
+
+    def item_status(self, item):
+        """
+        Given an Item object, return three numbers: (library, desk,
+        avail). Library is the total number of copies in the library
+        system; Desk is the number of copies at the designated reserves
+        desk; and Avail is the number of copies available for checkout at
+        the given moment. Note that 'library' includes 'desk' which
+        includes 'avail'. You may also return None if the item is
+        nonsensical (e.g. it is not a physical object, or it has no bib
+        ID).
+
+        Note, 'item.bib_id' is the item's bib_id, or None;
+        'item.item_type' will equal 'PHYS' for physical items;
+        'item.site.service_desk' is the ServiceDesk object associated with
+        the item. The ServiceDesk object has an 'external_id' attribute
+        which should represent the desk in the ILS.
+        """
+        if not item.bib_id:
+            return None
+        return self._item_status(item.bib_id)
+
+    CACHE_TIME = 300
+
+    @memoize(timeout=CACHE_TIME)
+    def _item_status(self, bib_id):
+        # At this point, status information does not require the opensrf
+        # bindings, I am not sure there is a use case where an evergreen
+        # site would not have access to these but will leave for now
+        # since there are no hardcoded references
+        try:
+            counts = E1(OPENSRF_COPY_COUNTS, bib_id, 1, 0)
+            lib = desk = avail = vol = 0
+            dueinfo = ''
+            callno  = ''
+            circmod = ''
+            alldues = []
+
+            for org, callnum, loc, stats in counts:
+                callprefix = ''
+                callsuffix = ''
+                if len(callno) == 0:
+                    callno = callnum
+                avail_here = stats.get(self.AVAILABLE, 0)
+                avail_here += stats.get(self.RESHELVING, 0)
+                anystatus_here = sum(stats.values())
+
+                # volume check - based on v.1, etc. in call number
+                voltest = re.search(r'\w*v\.\s?(\d+)', callnum)
+
+                # attachment test
+                attachtest = re.search(self.IS_ATTACHMENT, callnum)
+
+                if loc == settings.RESERVES_DESK_NAME:
+                    desk += anystatus_here
+                    avail += avail_here
+                    dueinfo = ''
+
+                    if (voltest and vol > 0 ):
+                        if (int(voltest.group(1)) > vol):
+                            callsuffix = "/" + callnum
+                        else:
+                            callprefix = callnum + "/"
+                    elif attachtest and callno.find(attachtest.group(0)) == -1:
+                        if len(callno) > 0:
+                            callsuffix = "/" + callnum
+                        else:
+                            callprefix = callnum
+                    else:
+                        callno = callnum
+
+                    lib += anystatus_here
+                    copyids = E1(OPENSRF_CN_CALL, bib_id, callnum, org)
+
+                    # we want to return the resource that will be returned first if
+                    # already checked out
+                    for copyid in copyids:
+                        circinfo = E1(OPENSRF_FLESHED2_CALL, copyid)
+
+                        thisloc = circinfo.get("location")
+                        if thisloc:
+                            thisloc = thisloc.get("name")
+
+                        if thisloc == settings.RESERVES_DESK_NAME:
+                            bringfw = attachtest
+
+                            # multiple volumes
+                            if voltest and callno.find(voltest.group(0)) == -1:
+                                bringfw = True
+
+                            if len(circmod) == 0:
+                                circmod = circinfo.get("circ_modifier")
+                            circs = circinfo.get("circulations")
+
+                            if circs and isinstance(circs, list):
+                                circ = circs[0]
+                                rawdate = circ.get("due_date")
+                                #remove offset info, %z is flakey for some reason
+                                rawdate = rawdate[:-5]
+                                duetime = time.strptime(rawdate, self.TIME_FORMAT)
+
+                            if (avail == 0 or bringfw) and circs and len(circs) > 0:
+                                if len(dueinfo) == 0 or bringfw:
+                                    earliestdue = duetime
+                                    if voltest:
+                                        if (int(voltest.group(1)) > vol):
+                                            if len(dueinfo) > 0:
+                                                dueinfo = dueinfo + "/"
+                                            dueinfo = dueinfo + voltest.group(0) + ': ' + time.strftime(self.DUE_FORMAT,earliestdue)
+                                        else:
+                                            tmpinfo = dueinfo
+                                            dueinfo = voltest.group(0) + ': ' + time.strftime(self.DUE_FORMAT,earliestdue)
+                                            if len(tmpinfo) > 0:
+                                                dueinfo = dueinfo + "/" + tmpinfo
+                                        callprefix = callsuffix = ''
+                                    elif attachtest:
+                                        tmpinfo = dueinfo
+                                        dueinfo = attachtest.group(0) + ': ' + time.strftime(self.DUE_FORMAT,earliestdue)
+                                        if len(callno) > 0:
+                                            callno = callno + '/' + callnum
+                                            callprefix = callsuffix = ''
+                                        else:
+                                            callno = callnum
+                                        if len(tmpinfo) > 0:
+                                            dueinfo = dueinfo + "/" + tmpinfo
+
+                                    if not bringfw:
+                                        dueinfo = time.strftime(self.DUE_FORMAT,earliestdue)
+                                        callno = callnum
+
+                                # way too wacky to sort out vols for this
+                                if duetime < earliestdue and not bringfw:
+                                    earliestdue = duetime
+                                    dueinfo = time.strftime(self.DUE_FORMAT,earliestdue)
+                                    callno = callnum
+
+                            alldisplay = callnum + ' (Available)'
+
+                            if circs and isinstance(circs, list):
+                                alldisplay = '%s (DUE: %s)' % (callnum, time.strftime(self.DUE_FORMAT,duetime))
+
+                            alldues.append(alldisplay)
+
+                        if voltest or attachtest:
+                            if callno.find(callprefix) == -1:
+                                callno = callprefix + callno
+                            if callno.find(callsuffix) == -1:
+                                callno = callno + callsuffix
+                        if voltest:
+                            vol = int(voltest.group(1))
+            return (lib, desk, avail, callno, dueinfo, circmod, alldues)
+        except:
+            print "due date/call problem: ", bib_id
+            print "*** print_exc:"
+            traceback.print_exc()
+            return None # fail silently in production if there's an opensrf or time related error.
+
+
+    # You'll need to define OSRF_CAT_SEARCH_ORG_UNIT, either by overriding its
+    # definition in your subclass, or by defining it in your
+    # local_settings.py.
+
+    OSRF_CAT_SEARCH_ORG_UNIT = getattr(settings, 'OSRF_CAT_SEARCH_ORG_UNIT', None)
+
+    def cat_search(self, query, start=1, limit=10):
+        barcode = 0
+        bibid	= 0
+        is_barcode = re.search('\d{14}', query)
+
+        if query.startswith(self.EG_BASE):
+            # query is an Evergreen URL
+            # snag the bibid at this point
+            params = dict([x.split("=") for x in query.split("&")])
+            for key in params.keys():
+                if key.find('?r') != -1:
+                    bibid = params[key]
+            results = M.marcxml_to_records(I.url_to_marcxml(query))
+            numhits = len(results)
+        elif is_barcode:
+            results = []
+            numhits = 0
+            barcode = query.strip()
+            bib = E1('open-ils.search.bib_id.by_barcode', barcode)
+            if bib:
+                bibid = bib
+                copy = E1('open-ils.supercat.record.object.retrieve', bib)
+                marc = copy[0]['marc']
+                # In some institutions' installations, 'marc' is a string; in
+                # others it's unicode. Convert to unicode if necessary.
+                if not isinstance(marc, unicode):
+                    marc = unicode(marc, 'utf-8')
+                tree = M.marcxml_to_records(marc)[0]
+                results.append(tree)
+                numhits = 1
+        else:
+            # query is an actual query
+            if self.USE_Z3950:
+                cat_host, cat_port, cat_db = settings.Z3950_CONFIG
+                results, numhits = PZ.search(cat_host, cat_port, cat_db, query, start, limit)
+            else:					# use opensrf
+                if not self.OSRF_CAT_SEARCH_ORG_UNIT:
+                    raise NotImplementedError, \
+                        'Your integration must provide a value for OSRF_CAT_SEARCH_ORG_UNIT.'
+
+                superpage = E1('open-ils.search.biblio.multiclass.query',
+                               {'org_unit': self.OSRF_CAT_SEARCH_ORG_UNIT,
+                                'depth': 1, 'limit': limit, 'offset': start-1,
+                                'visibility_limit': 3000,
+                                'default_class': 'keyword'},
+                               query, 1)
+                ids = [id for (id,) in superpage['ids']]
+                results = []
+                for rec in E1('open-ils.supercat.record.object.retrieve', ids):
+                    marc = rec['marc']
+                    # In some institutions' installations, 'marc' is a string; in
+                    # others it's unicode. Convert to unicode if necessary.
+                    if not isinstance(marc, unicode):
+                        marc = unicode(marc, 'utf-8')
+                    tree = M.marcxml_to_records(marc)[0]
+                    results.append(tree)
+                numhits = int(superpage['count'])
+        return results, numhits, bibid, barcode
+
+    def bib_id_to_marcxml(self, bib_id):
+        """
+        Given a bib_id, return a MARC record in MARCXML format. Return
+        None if the bib_id does not exist.
+        """
+        try:
+            xml = I.bib_id_to_marcxml(bib_id)
+            return ET.fromstring(xml)
+        except:
+            return None
+
+    def marc_to_bib_id(self, marc_string):
+        """
+        Given a MARC record, return either a bib ID or None, if no bib ID can be
+        found.
+        """
+        dct = M.marcxml_to_dictionary(marc_string)
+        bib_id = dct.get('901c')
+        return bib_id
+
+    def bib_id_to_url(self, bib_id):
+        """
+        Given a bib ID, return either a URL for examining the bib record, or None.
+        """
+        # TODO: move this to local_settings
+        if bib_id:
+            return ('%sopac/en-CA'
+                    '/skin/uwin/xml/rdetail.xml?r=%s&l=1&d=0' % (self.EG_BASE, bib_id))
+
+    if USE_Z3950:
+        # only if we are using Z39.50 for catalogue search. Against our Conifer
+        # Z39.50 server, results including accented characters are often seriously
+        # messed up. (Try searching for "montreal").
+        def get_better_copy_of_marc(self, marc_string):
+            """
+            This function takes a MARCXML record and returns either the same
+            record, or another instance of the same record from a different
+            source.
+
+            This is a hack. There is currently at least one Z39.50 server that
+            returns a MARCXML record with broken character encoding. This
+            function declares a point at which we can work around that server.
+            """
+            bib_id = self.marc_to_bib_id(marc_string)
+            better = self.bib_id_to_marcxml(bib_id)
+            # don't return the "better" record if there's no 901c in it...
+            if better and ('901c' in M.marcxml_to_dictionary(better)):
+                return better
+            return ET.fromstring(marc_string)
+
+    def marcxml_to_url(self, marc_string):
+        """
+        Given a MARC record, return either a URL (representing the
+        electronic resource) or None.
+
+        Typically this will be the 856$u value; but in Conifer, 856$9 and
+        856$u form an associative array, where $9 holds the institution
+        codes and $u holds the URLs.
+        """
+        # TODO: move this to local_settings
+        LIBCODE = 'OWA'					# Leddy
+        try:
+            dct           = M.marcxml_to_dictionary(marc_string)
+            words = lambda string: re.findall(r'\S+', string)
+            keys  = words(dct.get('8569'))
+            urls  = words(dct.get('856u'))
+            print 'KEYS:', keys
+            print 'URLS:', urls
+            return urls[keys.index(LIBCODE)]
+        except:
+            return None
+
+    @disable
+    def department_course_catalogue(self):
+        """
+        Return a list of rows representing all known, active courses and
+        the departments to which they belong. Each row should be a tuple
+        in the form: ('Department name', 'course-code', 'Course name').
+        """
+
+    @disable
+    def term_catalogue(self):
+        """
+        Return a list of rows representing all known terms. Each row
+        should be a tuple in the form: ('term-code', 'term-name',
+        'start-date', 'end-date'), where the dates are instances of the
+        datetime.date class.
+        """
+
+    @disable
+    def external_person_lookup(self, userid):
+        """
+        Given a userid, return either None (if the user cannot be found),
+        or a dictionary representing the user. The dictionary must contain
+        the keys ('given_name', 'surname') and should contain 'email' if
+        an email address is known, and 'patron_id' if a library-system ID
+        is known.
+        """
+
+    @disable
+    def external_memberships(self, userid):
+        """
+        Given a userid, return a list of dicts, representing the user's
+        memberships in known external groups. Each dict must include the
+        following key/value pairs:
+        'group': a group-code, externally defined;
+        'role':          the user's role in that group, one of (INSTR, ASSIST, STUDT).
+        """
+
+    @disable
+    def fuzzy_person_lookup(self, query, include_students=False):
+        """
+        Given a query, return a list of users who probably match the
+        query. The result is a list of (userid, display), where userid
+        is the campus userid of the person, and display is a string
+        suitable for display in a results-list. Include_students
+        indicates that students, and not just faculty/staff, should be
+        included in the results.
+        """
+    @disable
+    def derive_group_code_from_section(self, site, section):
+        """
+        This function is used to simplify common-case permission setting
+        on course sites. It takes a site and a section number/code, and
+        returns the most likely external group code. (This function will
+        probably check the site's term and course codes, and merge those
+        with the section code, to derive the group code.) Return None if a
+        valid, unambiguous group code cannot be generated.
+        """
+
+
+    @disable
+    def download_declaration(self):
+        """
+        Returns a string. The declaration to which students must agree when
+        downloading electronic documents. If not customized, a generic message
+        will be used.
+        """
+
+    @disable
+    def proxify_url(self, url):
+        """
+        Given a URL, determine whether the URL needs to be passed through
+        a reverse-proxy, and if so, return a modified URL that includes
+        the proxy. If not, return None.
+        """

Modified: servres/trunk/conifer/integration/uwindsor.py
===================================================================
--- servres/trunk/conifer/integration/uwindsor.py	2011-04-03 00:37:58 UTC (rev 1302)
+++ servres/trunk/conifer/integration/uwindsor.py	2011-04-03 00:38:02 UTC (rev 1303)
@@ -1,479 +1,153 @@
-# See conifer/syrup/integration.py for documentation.
-
 from conifer.libsystems import ezproxy
-from conifer.libsystems import marcxml as M
-from conifer.libsystems.evergreen import item_status as I
-from conifer.libsystems.evergreen.support import initialize, E1
-from conifer.libsystems.z3950 import pyz3950_search as PZ
-from datetime import date
-from django.conf import settings
-from memoization import memoize
-from xml.etree import ElementTree as ET
+from datetime           import date
+from evergreen_site     import EvergreenIntegration
 import csv
-import datetime
-import time
-import os
-import re
-import traceback
 import subprocess
 import uwindsor_campus_info
 import uwindsor_fuzzy_lookup
+from django.conf                          import settings
 
-# USE_Z3950: if True, use Z39.50 for catalogue search; if False, use OpenSRF.
-# Don't set this value directly here: rather, if there is a valid Z3950_CONFIG
-# settings in local_settings.py, then Z39.50 will be used.
 
-USE_Z3950 = getattr(settings, 'Z3950_CONFIG', None) is not None
+class UWindsorIntegration(EvergreenIntegration):
 
 
-OPENSRF_AUTHENTICATE      = "open-ils.auth.authenticate.complete"
-OPENSRF_AUTHENTICATE_INIT = "open-ils.auth.authenticate.init"
-OPENSRF_BATCH_UPDATE      = "open-ils.cat.asset.copy.fleshed.batch.update"
-OPENSRF_CIRC_UPDATE       = "open-ils.cstore open-ils.cstore.direct.action.circulation.update"
-OPENSRF_CLEANUP           = "open-ils.auth.session.delete"
-OPENSRF_CN_BARCODE        = "open-ils.circ.copy_details.retrieve.barcode.authoritative"
-OPENSRF_CN_CALL           = "open-ils.search.asset.copy.retrieve_by_cn_label"
-OPENSRF_COPY_COUNTS       = "open-ils.search.biblio.copy_counts.location.summary.retrieve"
-OPENSRF_FLESHED2_CALL     = "open-ils.search.asset.copy.fleshed2.retrieve"
-OPENSRF_FLESHEDCOPY_CALL  = "open-ils.search.asset.copy.fleshed.batch.retrieve.authoritative"
+    OSRF_CAT_SEARCH_ORG_UNIT = 106
 
-TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
-DUE_FORMAT  = "%b %d %Y, %r"
+    #---------------------------------------------------------------------------
+    # proxy server integration
 
-# regular expression to detect DVD, CD, CD-ROM, Guide, Booklet on the end of a
-# call number
-IS_ATTACHMENT = re.compile('\w*DVD\s?|\w*CD\s?|\w[Gg]uide\s?|\w[Bb]ooklet\s?|\w*CD\-ROM\s?')
+    ezproxy_service = ezproxy.EZProxyService(
+        settings.UWINDSOR_EZPROXY_HOST,
+        settings.UWINDSOR_EZPROXY_PASSWORD)
 
+    def proxify_url(self, url):
+        """
+        Given a URL, determine whether the URL needs to be passed through
+        a reverse-proxy, and if so, return a modified URL that includes
+        the proxy. If not, return None.
+        """
+        return self.ezproxy_service.proxify(url)
 
-def department_course_catalogue():
-    """
-    Return a list of rows representing all known, active courses and
-    the departments to which they belong. Each row should be a tuple
-    in the form: ('Department name', 'course-code', 'Course name').
-    """
-    url = 'http://cleo.uwindsor.ca/graham/courses.txt.gz'
-    p = subprocess.Popen('curl -s %s | gunzip -c' % url,
-                         shell=True, stdout=subprocess.PIPE)
-    reader = csv.reader(p.stdout)
-    catalogue = list(reader)
-    p.stdout.close()
-    return catalogue
 
-def term_catalogue():
-    """
-    Return a list of rows representing all known terms. Each row
-    should be a tuple in the form: ('term-code', 'term-name',
-    'start-date', 'end-date'), where the dates are instances of the
-    datetime.date class.
-    """
-    # TODO: make this algorithmic.
-    return [
-        ('2011S', '2011 Summer', date(2011,5,1), date(2011,9,1)),
-        ('2011F', '2011 Fall', date(2011,9,1), date(2011,12,31)),
-        ]
+    #---------------------------------------------------------------------------
+    # campus information
 
+    def department_course_catalogue(self):
+        """
+        Return a list of rows representing all known, active courses and
+        the departments to which they belong. Each row should be a tuple
+        in the form: ('Department name', 'course-code', 'Course name').
+        """
+        url = 'http://cleo.uwindsor.ca/graham/courses.txt.gz'
+        p = subprocess.Popen('curl -s %s | gunzip -c' % url,
+                             shell=True, stdout=subprocess.PIPE)
+        reader = csv.reader(p.stdout)
+        catalogue = list(reader)
+        p.stdout.close()
+        return catalogue
 
-#--------------------------------------------------
-# ILS integration
+    def term_catalogue(self):
+        """
+        Return a list of rows representing all known terms. Each row
+        should be a tuple in the form: ('term-code', 'term-name',
+        'start-date', 'end-date'), where the dates are instances of the
+        datetime.date class.
+        """
+        # TODO: make this algorithmic.
+        return [
+            ('2011S', '2011 Summer', date(2011,5,1), date(2011,9,1)),
+            ('2011F', '2011 Fall', date(2011,9,1), date(2011,12,31)),
+            ]
 
-EG_BASE = 'http://%s/' % settings.EVERGREEN_GATEWAY_SERVER
-initialize(EG_BASE)
+    def external_person_lookup(self, userid):
+        """
+        Given a userid, return either None (if the user cannot be found),
+        or a dictionary representing the user. The dictionary must contain
+        the keys ('given_name', 'surname') and should contain 'email' if
+        an email address is known, and 'patron_id' if a library-system ID
+        is known.
+        """
+        return uwindsor_campus_info.call('person_lookup', userid)
 
 
-# Item status stuff
+    def external_memberships(self, userid):
+        """
+        Given a userid, return a list of dicts, representing the user's
+        memberships in known external groups. Each dict must include the
+        following key/value pairs:
+        'group': a group-code, externally defined;
+        'role':          the user's role in that group, one of (INSTR, ASSIST, STUDT).
+        """
+        memberships = uwindsor_campus_info.call('membership_ids', userid)
+        for m in memberships:
+            m['role'] = self._decode_role(m['role'])
+        return memberships
 
-STATUS_DECODE = [(str(x['id']), x['name'])
-                 for x in E1('open-ils.search.config.copy_status.retrieve.all')]
-AVAILABLE = [id for id, name in STATUS_DECODE if name == 'Available'][0]
-RESHELVING = [id for id, name in STATUS_DECODE if name == 'Reshelving'][0]
+    def _decode_role(self, role):
+        if role == 'Instructor':
+            return 'INSTR'
+        else:
+            return 'STUDT'
 
-def item_status(item):
-    """
-    Given an Item object, return three numbers: (library, desk,
-    avail). Library is the total number of copies in the library
-    system; Desk is the number of copies at the designated reserves
-    desk; and Avail is the number of copies available for checkout at
-    the given moment. Note that 'library' includes 'desk' which
-    includes 'avail'. You may also return None if the item is
-    nonsensical (e.g. it is not a physical object, or it has no bib
-    ID).
+    def fuzzy_person_lookup(self, query, include_students=False):
+        """
+        Given a query, return a list of users who probably match the
+        query. The result is a list of (userid, display), where userid
+        is the campus userid of the person, and display is a string
+        suitable for display in a results-list. Include_students
+        indicates that students, and not just faculty/staff, should be
+        included in the results.
+        """
+        # Note, our 'include_students' option only matches students on exact
+        # userids. That is, fuzzy matching only works for staff, faculty, and
+        # other non-student roles.
 
-    Note, 'item.bib_id' is the item's bib_id, or None;
-    'item.item_type' will equal 'PHYS' for physical items;
-    'item.site.service_desk' is the ServiceDesk object associated with
-    the item. The ServiceDesk object has an 'external_id' attribute
-    which should represent the desk in the ILS.
-    """
-    if not item.bib_id:
-        return None
-    return _item_status(item.bib_id)
+        filter	= uwindsor_fuzzy_lookup.build_filter(query, include_students)
+        results = uwindsor_fuzzy_lookup.search(filter)
 
-CACHE_TIME = 300
+        out = []
+        for res in results:
+            if not 'employeeType' in res:
+                res['employeeType'] = 'Student' # a 99% truth!
+            display = ('%(givenName)s %(sn)s. %(employeeType)s, '
+                       '%(uwinDepartment)s. <%(mail)s>. [%(uid)s]') % res
+            out.append((res['uid'], display))
+        return out
 
- at memoize(timeout=CACHE_TIME)
-def _item_status(bib_id):
-    # At this point, status information does not require the opensrf
-    # bindings, I am not sure there is a use case where an evergreen
-    # site would not have access to these but will leave for now
-    # since there are no hardcoded references
-    try:
-        counts = E1(OPENSRF_COPY_COUNTS, bib_id, 1, 0)
-        lib = desk = avail = vol = 0
-        dueinfo = ''
-        callno  = ''
-        circmod = ''
-        alldues = []
 
-        for org, callnum, loc, stats in counts:
-            callprefix = ''
-            callsuffix = ''
-            if len(callno) == 0:
-                callno = callnum
-            avail_here = stats.get(AVAILABLE, 0)
-            avail_here += stats.get(RESHELVING, 0)
-            anystatus_here = sum(stats.values())
+    def derive_group_code_from_section(self, site, section):
+        """
+        This function is used to simplify common-case permission setting
+        on course sites. It takes a site and a section number/code, and
+        returns the most likely external group code. (This function will
+        probably check the site's term and course codes, and merge those
+        with the section code, to derive the group code.) Return None if a
+        valid, unambiguous group code cannot be generated.
+        """
+        try:
+            section = int(section)
+        except:
+            return None
 
-            # volume check - based on v.1, etc. in call number
-            voltest = re.search(r'\w*v\.\s?(\d+)', callnum)
+        return '%s-%s-%s' % (site.course.code.replace('-', ''),
+                             section,
+                             site.start_term.code)
 
-            # attachment test
-            attachtest = re.search(IS_ATTACHMENT, callnum)
+    #---------------------------------------------------------------------------
+    # copyright/permissions
 
-            if loc == settings.RESERVES_DESK_NAME:
-                desk += anystatus_here
-                avail += avail_here
-                dueinfo = ''
-
-                if (voltest and vol > 0 ):
-                    if (int(voltest.group(1)) > vol):
-                        callsuffix = "/" + callnum
-                    else:
-                        callprefix = callnum + "/"
-                elif attachtest and callno.find(attachtest.group(0)) == -1:
-                    if len(callno) > 0:
-                        callsuffix = "/" + callnum
-                    else:
-                        callprefix = callnum
-                else:
-                    callno = callnum
-
-                lib += anystatus_here
-                copyids = E1(OPENSRF_CN_CALL, bib_id, callnum, org)
-
-                # we want to return the resource that will be returned first if
-                # already checked out
-                for copyid in copyids:
-                    circinfo = E1(OPENSRF_FLESHED2_CALL, copyid)
-
-                    thisloc = circinfo.get("location")
-                    if thisloc:
-                        thisloc = thisloc.get("name")
-
-                    if thisloc == settings.RESERVES_DESK_NAME:
-                        bringfw = attachtest
-
-                        # multiple volumes
-                        if voltest and callno.find(voltest.group(0)) == -1:
-                            bringfw = True
-
-                        if len(circmod) == 0:
-                            circmod = circinfo.get("circ_modifier")
-                        circs = circinfo.get("circulations")
-
-                        if circs and isinstance(circs, list):
-                            circ = circs[0]
-                            rawdate = circ.get("due_date")
-                            #remove offset info, %z is flakey for some reason
-                            rawdate = rawdate[:-5]
-                            duetime = time.strptime(rawdate, TIME_FORMAT)
-
-                        if (avail == 0 or bringfw) and circs and len(circs) > 0:
-                            if len(dueinfo) == 0 or bringfw:
-                                earliestdue = duetime
-                                if voltest:
-                                    if (int(voltest.group(1)) > vol):
-                                        if len(dueinfo) > 0:
-                                            dueinfo = dueinfo + "/"
-                                        dueinfo = dueinfo + voltest.group(0) + ': ' + time.strftime(DUE_FORMAT,earliestdue)
-                                    else:
-                                        tmpinfo = dueinfo
-                                        dueinfo = voltest.group(0) + ': ' + time.strftime(DUE_FORMAT,earliestdue)
-                                        if len(tmpinfo) > 0:
-                                            dueinfo = dueinfo + "/" + tmpinfo
-                                    callprefix = callsuffix = ''
-                                elif attachtest:
-                                    tmpinfo = dueinfo
-                                    dueinfo = attachtest.group(0) + ': ' + time.strftime(DUE_FORMAT,earliestdue)
-                                    if len(callno) > 0:
-                                        callno = callno + '/' + callnum
-                                        callprefix = callsuffix = ''
-                                    else:
-                                        callno = callnum
-                                    if len(tmpinfo) > 0:
-                                        dueinfo = dueinfo + "/" + tmpinfo
-
-                                if not bringfw:
-                                    dueinfo = time.strftime(DUE_FORMAT,earliestdue)
-                                    callno = callnum
-
-                            # way too wacky to sort out vols for this
-                            if duetime < earliestdue and not bringfw:
-                                earliestdue = duetime
-                                dueinfo = time.strftime(DUE_FORMAT,earliestdue)
-                                callno = callnum
-
-                        alldisplay = callnum + ' (Available)'
-
-                        if circs and isinstance(circs, list):
-                            alldisplay = '%s (DUE: %s)' % (callnum, time.strftime(DUE_FORMAT,duetime))
-
-                        alldues.append(alldisplay)
-
-                    if voltest or attachtest:
-                        if callno.find(callprefix) == -1:
-                            callno = callprefix + callno
-                        if callno.find(callsuffix) == -1:
-                            callno = callno + callsuffix
-                    if voltest:
-                        vol = int(voltest.group(1))
-        return (lib, desk, avail, callno, dueinfo, circmod, alldues)
-    except:
-        print "due date/call problem: ", bib_id
-        print "*** print_exc:"
-        traceback.print_exc()
-        return None # fail silently in production if there's an opensrf or time related error.
-
-CAT_SEARCH_ORG_UNIT = 106
-
-def cat_search(query, start=1, limit=10):
-    barcode = 0
-    bibid	= 0
-    is_barcode = re.search('\d{14}', query)
-
-    if query.startswith(EG_BASE):
-        # query is an Evergreen URL
-        # snag the bibid at this point
-        params = dict([x.split("=") for x in query.split("&")])
-        for key in params.keys():
-            if key.find('?r') != -1:
-                bibid = params[key]
-        results = M.marcxml_to_records(I.url_to_marcxml(query))
-        numhits = len(results)
-    elif is_barcode:
-        results = []
-        numhits = 0
-        barcode = query.strip()
-        bib = E1('open-ils.search.bib_id.by_barcode', barcode)
-        if bib:
-            bibid = bib
-            copy = E1('open-ils.supercat.record.object.retrieve', bib)
-            marc = copy[0]['marc']
-            # In some institutions' installations, 'marc' is a string; in
-            # others it's unicode. Convert to unicode if necessary.
-            if not isinstance(marc, unicode):
-                marc = unicode(marc, 'utf-8')
-            tree = M.marcxml_to_records(marc)[0]
-            results.append(tree)
-            numhits = 1
-    else:
-        # query is an actual query
-        if USE_Z3950:
-            cat_host, cat_port, cat_db = settings.Z3950_CONFIG
-            results, numhits = PZ.search(cat_host, cat_port, cat_db, query, start, limit)
-        else:					# use opensrf
-            superpage = E1('open-ils.search.biblio.multiclass.query',
-                           {'org_unit': CAT_SEARCH_ORG_UNIT,
-                            'depth': 1, 'limit': limit, 'offset': start-1,
-                            'visibility_limit': 3000,
-                            'default_class': 'keyword'},
-                           query, 1)
-            ids = [id for (id,) in superpage['ids']]
-            results = []
-            for rec in E1('open-ils.supercat.record.object.retrieve', ids):
-                marc = rec['marc']
-                # In some institutions' installations, 'marc' is a string; in
-                # others it's unicode. Convert to unicode if necessary.
-                if not isinstance(marc, unicode):
-                    marc = unicode(marc, 'utf-8')
-                tree = M.marcxml_to_records(marc)[0]
-                results.append(tree)
-            numhits = int(superpage['count'])
-    return results, numhits, bibid, barcode
-
-def bib_id_to_marcxml(bib_id):
-    """
-    Given a bib_id, return a MARC record in MARCXML format. Return
-    None if the bib_id does not exist.
-    """
-    try:
-        xml = I.bib_id_to_marcxml(bib_id)
-        return ET.fromstring(xml)
-    except:
-        return None
-
-def marc_to_bib_id(marc_string):
-    """
-    Given a MARC record, return either a bib ID or None, if no bib ID can be
-    found.
-    """
-    dct = M.marcxml_to_dictionary(marc_string)
-    bib_id = dct.get('901c')
-    return bib_id
-
-def bib_id_to_url(bib_id):
-    """
-    Given a bib ID, return either a URL for examining the bib record, or None.
-    """
-    # TODO: move this to local_settings
-    if bib_id:
-        return ('%sopac/en-CA'
-                '/skin/uwin/xml/rdetail.xml?r=%s&l=1&d=0' % (EG_BASE, bib_id))
-
-if USE_Z3950:
-    # only if we are using Z39.50 for catalogue search. Against our Conifer
-    # Z39.50 server, results including accented characters are often seriously
-    # messed up. (Try searching for "montreal").
-    def get_better_copy_of_marc(marc_string):
+    def download_declaration(self):
         """
-        This function takes a MARCXML record and returns either the same
-        record, or another instance of the same record from a different
-        source.
-
-        This is a hack. There is currently at least one Z39.50 server that
-        returns a MARCXML record with broken character encoding. This
-        function declares a point at which we can work around that server.
+        Returns a string. The declaration to which students must agree when
+        downloading electronic documents. If not customized, a generic message
+        will be used.
         """
-        bib_id = marc_to_bib_id(marc_string)
-        better = bib_id_to_marcxml(bib_id)
-        # don't return the "better" record if there's no 901c in it...
-        if better and ('901c' in M.marcxml_to_dictionary(better)):
-            return better
-        return ET.fromstring(marc_string)
+        # as per Joan Dalton, 2010-12-21.
+        # TODO: move this to local_settings
+        return ("I warrant that I am a student of the University of Windsor "
+                "enrolled in a course of instruction. By pressing the "
+                "'Request' button below, I am requesting a digital copy of a "
+                "reserve reading for research, private study, review or criticism "
+                "and that I will not use the copy for any other purpose, nor "
+                "will I transmit the copy to any third party.")
 
-def marcxml_to_url(marc_string):
-    """
-    Given a MARC record, return either a URL (representing the
-    electronic resource) or None.
-
-    Typically this will be the 856$u value; but in Conifer, 856$9 and
-    856$u form an associative array, where $9 holds the institution
-    codes and $u holds the URLs.
-    """
-    # TODO: move this to local_settings
-    LIBCODE = 'OWA'					# Leddy
-    try:
-        dct           = M.marcxml_to_dictionary(marc_string)
-        words = lambda string: re.findall(r'\S+', string)
-        keys  = words(dct.get('8569'))
-        urls  = words(dct.get('856u'))
-        print 'KEYS:', keys
-        print 'URLS:', urls
-        return urls[keys.index(LIBCODE)]
-    except:
-        return None
-
-
-def external_person_lookup(userid):
-    """
-    Given a userid, return either None (if the user cannot be found),
-    or a dictionary representing the user. The dictionary must contain
-    the keys ('given_name', 'surname') and should contain 'email' if
-    an email address is known, and 'patron_id' if a library-system ID
-    is known.
-    """
-    return uwindsor_campus_info.call('person_lookup', userid)
-
-
-def external_memberships(userid):
-    """
-    Given a userid, return a list of dicts, representing the user's
-    memberships in known external groups. Each dict must include the
-    following key/value pairs:
-    'group': a group-code, externally defined;
-    'role':          the user's role in that group, one of (INSTR, ASSIST, STUDT).
-    """
-    memberships = uwindsor_campus_info.call('membership_ids', userid)
-    for m in memberships:
-        m['role'] = decode_role(m['role'])
-    return memberships
-
-def decode_role(role):
-    if role == 'Instructor':
-        return 'INSTR'
-    else:
-        return 'STUDT'
-
-def fuzzy_person_lookup(query, include_students=False):
-    """
-    Given a query, return a list of users who probably match the
-    query. The result is a list of (userid, display), where userid
-    is the campus userid of the person, and display is a string
-    suitable for display in a results-list. Include_students
-    indicates that students, and not just faculty/staff, should be
-    included in the results.
-    """
-    # Note, our 'include_students' option only matches students on exact
-    # userids. That is, fuzzy matching only works for staff, faculty, and
-    # other non-student roles.
-
-    filter	= uwindsor_fuzzy_lookup.build_filter(query, include_students)
-    results = uwindsor_fuzzy_lookup.search(filter)
-
-    out = []
-    for res in results:
-        if not 'employeeType' in res:
-            res['employeeType'] = 'Student' # a 99% truth!
-        display = ('%(givenName)s %(sn)s. %(employeeType)s, '
-                   '%(uwinDepartment)s. <%(mail)s>. [%(uid)s]') % res
-        out.append((res['uid'], display))
-    return out
-
-
-def derive_group_code_from_section(site, section):
-    """
-    This function is used to simplify common-case permission setting
-    on course sites. It takes a site and a section number/code, and
-    returns the most likely external group code. (This function will
-    probably check the site's term and course codes, and merge those
-    with the section code, to derive the group code.) Return None if a
-    valid, unambiguous group code cannot be generated.
-    """
-    try:
-        section = int(section)
-    except:
-        return None
-
-    return '%s-%s-%s' % (site.course.code.replace('-', ''),
-                         section,
-                         site.start_term.code)
-
-#--------------------------------------------------
-# proxy server integration
-
-ezproxy_service = ezproxy.EZProxyService(
-    settings.UWINDSOR_EZPROXY_HOST,
-    settings.UWINDSOR_EZPROXY_PASSWORD)
-
-def proxify_url(url):
-    """
-    Given a URL, determine whether the URL needs to be passed through
-    a reverse-proxy, and if so, return a modified URL that includes
-    the proxy. If not, return None.
-    """
-    return ezproxy_service.proxify(url)
-
-
-def download_declaration():
-    """
-    Returns a string. The declaration to which students must agree when
-    downloading electronic documents. If not customized, a generic message
-    will be used.
-    """
-    # as per Joan Dalton, 2010-12-21.
-    # TODO: move this to local_settings
-    return ("I warrant that I am a student of the University of Windsor "
-            "enrolled in a course of instruction. By pressing the "
-            "'Request' button below, I am requesting a digital copy of a "
-            "reserve reading for research, private study, review or criticism "
-            "and that I will not use the copy for any other purpose, nor "
-            "will I transmit the copy to any third party.")

Modified: servres/trunk/conifer/local_settings.py.example
===================================================================
--- servres/trunk/conifer/local_settings.py.example	2011-04-03 00:37:58 UTC (rev 1302)
+++ servres/trunk/conifer/local_settings.py.example	2011-04-03 00:38:02 UTC (rev 1303)
@@ -63,10 +63,10 @@
 # SITE_DEFAULT_ACCESS_LEVEL = 'MEMBR'
 
 #----------------------------------------------------------------------
-# INTEGRATION_MODULE: name of a module to import after the database
-# models have been initialized. This can be used for defining 'hook'
-# functions, and other late initializations. 
-# See the 'conifer.syrup.integration' module for more information.
+# INTEGRATION_CLASS: name of a class to instantiate after the database models
+# have been initialized. This can be used for defining 'hook' functions, and
+# other late initializations. See the 'conifer.syrup.integration' module for
+# more information.
 
-INTEGRATION_MODULE = 'conifer.integration.uwindsor'
+INTEGRATION_CLASS = 'conifer.integration.uwindsor.UWindsorIntegration'
 

Modified: servres/trunk/conifer/plumbing/hooksystem.py
===================================================================
--- servres/trunk/conifer/plumbing/hooksystem.py	2011-04-03 00:37:58 UTC (rev 1302)
+++ servres/trunk/conifer/plumbing/hooksystem.py	2011-04-03 00:38:02 UTC (rev 1303)
@@ -1,10 +1,13 @@
-# TODO: decide whether or not to use this!
 
-import warnings
-import conifer.syrup.integration as HOOKS
+HOOKS = None
 
-__all__ = ['callhook', 'callhook_required', 'gethook']
+__all__ = ['callhook', 'callhook_required', 'gethook', 'initialize_hooks']
 
+def initialize_hooks(obj):
+    global HOOKS
+    assert HOOKS is None
+    HOOKS = obj
+
 def gethook(name, default=None):
     return getattr(HOOKS, name, None) or default
 

Modified: servres/trunk/conifer/syrup/integration.py
===================================================================
--- servres/trunk/conifer/syrup/integration.py	2011-04-03 00:37:58 UTC (rev 1302)
+++ servres/trunk/conifer/syrup/integration.py	2011-04-03 00:38:02 UTC (rev 1303)
@@ -1,159 +1,157 @@
-# this is a placeholder module, for the definitions in the
-# INTEGRATION_MODULE defined in local_settings.py. 
+# This module documents Syrup's integration points. Your local integrations
+# should be defined in a class in another module, referred to by name using
+# the INTEGRATION_CLASS setting in your local_settings.py.
 
-# Please do not define anything in this file. It will be automatically
-# populated once confier.syrup.models has been evaluated.
+# Please do not define anything in this file. It is here for documentation
+# purposes only. You are not required to subclass Integration when you
+# write your own integration code.
 
+
 def disable(func):
     return None
 
 
- at disable
-def can_create_sites(user):
-    """
-    Return True if this User object represents a person who should be
-    allowed to create new course-reserve sites. Note that users marked
-    as 'staff' are always allowed to create new sites.
-    """
+class Integration(object):
 
+    @disable
+    def can_create_sites(user):
+        """
+        Return True if this User object represents a person who should be
+        allowed to create new course-reserve sites. Note that users marked
+        as 'staff' are always allowed to create new sites.
+        """
 
- at disable
-def department_course_catalogue():
-    """
-    Return a list of rows representing all known, active courses and
-    the departments to which they belong. Each row should be a tuple
-    in the form: ('Department name', 'course-code', 'Course name').
-    """
+    @disable
+    def department_course_catalogue(self):
+        """
+        Return a list of rows representing all known, active courses and
+        the departments to which they belong. Each row should be a tuple
+        in the form: ('Department name', 'course-code', 'Course name').
+        """
 
+    @disable
+    def term_catalogue(self):
+        """
+        Return a list of rows representing all known terms. Each row
+        should be a tuple in the form: ('term-code', 'term-name',
+        'start-date', 'end-date'), where the dates are instances of the
+        datetime.date class.
+        """
 
- at disable
-def term_catalogue():
-    """
-    Return a list of rows representing all known terms. Each row
-    should be a tuple in the form: ('term-code', 'term-name',
-    'start-date', 'end-date'), where the dates are instances of the
-    datetime.date class.
-    """
+    @disable
+    def cat_search(self, query, start=1, limit=10):
+        """
+        Given a query, and optional start/limit values, return a tuple
+        (results, numhits). Results is a list of
+        xml.etree.ElementTree.Element instances. Each instance is a
+        MARCXML '<{http://www.loc.gov/MARC21/slim}record>'
+        element. Numhits is the total number of hits found against the
+        search, not simply the size of the results lists.
+        """
 
+    @disable
+    def item_status(self, item):
+        """
+        Given an Item object, return three numbers: (library, desk,
+        avail). Library is the total number of copies in the library
+        system; Desk is the number of copies at the designated reserves
+        desk; and Avail is the number of copies available for checkout at
+        the given moment. Note that 'library' includes 'desk' which
+        includes 'avail'. You may also return None if the item is
+        nonsensical (e.g. it is not a physical object, or it has no bib
+        ID).
 
- at disable
-def cat_search(query, start=1, limit=10):
-    """
-    Given a query, and optional start/limit values, return a tuple
-    (results, numhits). Results is a list of
-    xml.etree.ElementTree.Element instances. Each instance is a
-    MARCXML '<{http://www.loc.gov/MARC21/slim}record>'
-    element. Numhits is the total number of hits found against the
-    search, not simply the size of the results lists.
-    """
+        Note, 'item.bib_id' is the item's bib_id, or None;
+        'item.item_type' will equal 'PHYS' for physical items;
+        'item.site.service_desk' is the ServiceDesk object associated with
+        the item. The ServiceDesk object has an 'external_id' attribute
+        which should represent the desk in the ILS.
+        """
 
+    @disable
+    def bib_id_to_marcxml(self, bib_id):
+        """
+        Given a bib_id, return a MARC record in MARCXML format. Return
+        None if the bib_id does not exist.
+        """
 
- at disable
-def item_status(item):
-    """
-    Given an Item object, return three numbers: (library, desk,
-    avail). Library is the total number of copies in the library
-    system; Desk is the number of copies at the designated reserves
-    desk; and Avail is the number of copies available for checkout at
-    the given moment. Note that 'library' includes 'desk' which
-    includes 'avail'. You may also return None if the item is
-    nonsensical (e.g. it is not a physical object, or it has no bib
-    ID).
-    
-    Note, 'item.bib_id' is the item's bib_id, or None;
-    'item.item_type' will equal 'PHYS' for physical items;
-    'item.site.service_desk' is the ServiceDesk object associated with
-    the item. The ServiceDesk object has an 'external_id' attribute
-    which should represent the desk in the ILS.
-    """
+    @disable
+    def get_better_copy_of_marc(self, marc_string):
+        """
+        This function takes a MARCXML record and returns either the same
+        record, or another instance of the same record from a different
+        source. 
 
+        This is a hack. There is currently at least one Z39.50 server that
+        returns a MARCXML record with broken character encoding. This
+        function declares a point at which we can work around that server.
+        """
 
- at disable
-def bib_id_to_marcxml(bib_id):
-    """
-    Given a bib_id, return a MARC record in MARCXML format. Return
-    None if the bib_id does not exist.
-    """
+    @disable
+    def marcxml_to_url(self, marc_string):
+        """
+        Given a MARC record, return either a URL (representing the
+        electronic resource) or None.
 
-    
- at disable
-def get_better_copy_of_marc(marc_string):
-    """
-    This function takes a MARCXML record and returns either the same
-    record, or another instance of the same record from a different
-    source. 
+        Typically this will be the 856$u value; but in Conifer, 856$9 and
+        856$u form an associative array, where $9 holds the institution
+        codes and $u holds the URLs.
+        """
 
-    This is a hack. There is currently at least one Z39.50 server that
-    returns a MARCXML record with broken character encoding. This
-    function declares a point at which we can work around that server.
-    """
+    @disable
+    def external_person_lookup(self, userid):
+        """
+        Given a userid, return either None (if the user cannot be found),
+        or a dictionary representing the user. The dictionary must contain
+        the keys ('given_name', 'surname') and should contain 'email' if
+        an email address is known.
+        """
 
+    @disable
+    def external_memberships(self, userid):
+        """
+        Given a userid, return a list of dicts,
+        representing the user's memberships in known external groups.
+        Each dict must include the following key/value pairs:
+        'group': a group-code, externally defined;
+        'role':  the user's role in that group, one of (INSTR, ASSIST, STUDT).
+        """
 
- at disable
-def marcxml_to_url(marc_string):
-    """
-    Given a MARC record, return either a URL (representing the
-    electronic resource) or None.
+    @disable
+    def user_needs_decoration(self, user_obj):
+        """
+        User objects are sometimes created automatically, with only a
+        username. This function determines whether it would be fruitful to
+        "decorate" the User object with, e.g., a given name, surname, and
+        email address. It doesn't perform the decoration, it simply tests
+        whether the current user object is "incomplete." Another hook
+        'external_person_lookup,' is used by Syrup to fetch the personal
+        information when needed.
+        """
 
-    Typically this will be the 856$u value; but in Conifer, 856$9 and
-    856$u form an associative array, where $9 holds the institution
-    codes and $u holds the URLs.
-    """
+    @disable
+    def derive_group_code_from_section(self, site, section):
+        """
+        This function is used to simplify common-case permission setting
+        on course sites. It takes a site and a section number/code, and
+        returns the most likely external group code. (This function will
+        probably check the site's term and course codes, and merge those
+        with the section code, to derive the group code.) Return None if a
+        valid, unambiguous group code cannot be generated.
+        """
 
- at disable
-def external_person_lookup(userid):
-    """
-    Given a userid, return either None (if the user cannot be found),
-    or a dictionary representing the user. The dictionary must contain
-    the keys ('given_name', 'surname') and should contain 'email' if
-    an email address is known.
-    """
+    @disable
+    def proxify_url(self, url):
+        """
+        Given a URL, determine whether the URL needs to be passed through
+        a reverse-proxy, and if so, return a modified URL that includes
+        the proxy. If not, return None.
+        """
 
- at disable
-def external_memberships(userid):
-    """
-    Given a userid, return a list of dicts,
-    representing the user's memberships in known external groups.
-    Each dict must include the following key/value pairs:
-    'group': a group-code, externally defined;
-    'role':  the user's role in that group, one of (INSTR, ASSIST, STUDT).
-    """
-
- at disable
-def user_needs_decoration(user_obj):
-    """
-    User objects are sometimes created automatically, with only a
-    username. This function determines whether it would be fruitful to
-    "decorate" the User object with, e.g., a given name, surname, and
-    email address. It doesn't perform the decoration, it simply tests
-    whether the current user object is "incomplete." Another hook
-    'external_person_lookup,' is used by Syrup to fetch the personal
-    information when needed.
-    """
-
- at disable
-def derive_group_code_from_section(site, section):
-    """
-    This function is used to simplify common-case permission setting
-    on course sites. It takes a site and a section number/code, and
-    returns the most likely external group code. (This function will
-    probably check the site's term and course codes, and merge those
-    with the section code, to derive the group code.) Return None if a
-    valid, unambiguous group code cannot be generated.
-    """
-
- at disable
-def proxify_url(url):
-    """
-    Given a URL, determine whether the URL needs to be passed through
-    a reverse-proxy, and if so, return a modified URL that includes
-    the proxy. If not, return None.
-    """
-
- at disable
-def download_declaration():
-    """
-    Returns a string. The declaration to which students must agree when
-    downloading electronic documents. If not customized, a generic message
-    will be used.
-    """
+    @disable
+    def download_declaration(self):
+        """
+        Returns a string. The declaration to which students must agree when
+        downloading electronic documents. If not customized, a generic message
+        will be used.
+        """

Modified: servres/trunk/conifer/syrup/models.py
===================================================================
--- servres/trunk/conifer/syrup/models.py	2011-04-03 00:37:58 UTC (rev 1302)
+++ servres/trunk/conifer/syrup/models.py	2011-04-03 00:38:02 UTC (rev 1303)
@@ -78,7 +78,7 @@
     def maybe_refresh_external_memberships(self):
         profile = self.get_profile()
         last_checked = profile.external_memberships_checked
-        if (not last_checked or last_checked < 
+        if (not last_checked or last_checked <
             (datetime.now() - self.EXT_MEMBERSHIP_CHECK_FREQUENCY)):
             added, dropped = external_groups.reconcile_user_memberships(self)
             profile.external_memberships_checked = datetime.now()
@@ -99,7 +99,7 @@
             return
 
         # does this user need decorating?
-        dectest = gethook('user_needs_decoration', 
+        dectest = gethook('user_needs_decoration',
                           default=lambda user: user.last_name == '')
         if not dectest(self):
             return
@@ -116,7 +116,7 @@
 
         if 'patron_id' in dir_entry:
             # note, we overrode user.get_profile() to automatically create
-            # missing profiles. 
+            # missing profiles.
             self.get_profile().ils_userid = dir_entry['patron_id']
             profile.save()
 
@@ -256,7 +256,7 @@
         """
         Returns the start term (typically the term thought of as 'the' term of
         the site).
-        
+
         Whenever possible, use the explicit 'start_term' attribute rather than
         the 'term' property.
         """
@@ -432,7 +432,7 @@
             return user.is_staff or self.is_member(user)
         else:
             return self.is_open_to(user)
-        
+
     @classmethod
     def taught_by(cls, user):
         """Return a set of Sites for which this user is an Instructor."""
@@ -482,9 +482,9 @@
     # TODO: add constraints to ensure that each Site has
     # exactly one Group with external_id=NULL, and that (site,
     # external_id) is unique forall external_id != NULL.
-    
+
     # TODO: On second thought, for now make it:
-    # external_id is unique forall external_id != NULL. 
+    # external_id is unique forall external_id != NULL.
     # That is, only one Site may use a given external group.
 
     site = m.ForeignKey(Site)
@@ -614,7 +614,7 @@
     # Options for evergreen updates
     EVERGREEN_UPDATE_CHOICES = settings.UPDATE_CHOICES
 
-    evergreen_update = m.CharField(max_length=4, 
+    evergreen_update = m.CharField(max_length=4,
                                    choices=EVERGREEN_UPDATE_CHOICES,
                                    default='One')
 
@@ -627,10 +627,12 @@
         ('AV', 'available to students'),
         ]
 
-    copyright_status = m.CharField(max_length=2, 
+    copyright_status = m.CharField(max_length=2,
                                    choices=COPYRIGHT_STATUS_CHOICES,
                                    default='UK')
 
+    # TODO: fixme, the CIRC stuff here is very Leddy specific.
+
     # Options for circ modifiers
     CIRC_MODIFIER_CHOICES = [
         ('CIRC', 'Normal'),
@@ -640,9 +642,9 @@
         ('RSV7', '7 Day'),
         ]
 
-    circ_modifier = m.CharField(max_length=10, 
-                                   choices=CIRC_MODIFIER_CHOICES,
-                                   default='RSV2')
+    circ_modifier = m.CharField(max_length=10,
+                                choices=CIRC_MODIFIER_CHOICES,
+                                default='RSV2', blank=True)
 
     # Options for circ desk
     CIRC_DESK_CHOICES = [
@@ -650,9 +652,9 @@
         ('598', 'Circulating Collection'),
         ]
 
-    circ_desk = m.CharField(max_length=5, 
-                                   choices=CIRC_DESK_CHOICES,
-                                   default='631')
+    circ_desk = m.CharField(max_length=5,
+                            choices=CIRC_DESK_CHOICES,
+                            default='631', blank=True)
 
     ITEMTYPE_CHOICES = [
         # From http://www.oclc.org/bibformats/en/fixedfield/type.shtm.
@@ -763,7 +765,7 @@
                     script,
                     self.site_id, self.id,
                     self.fileobj.name.split('/')[-1]))
-            
+
     def item_url(self, suffix='', force_local=False):
         if self.item_type == 'URL' and suffix == '' and not force_local:
             return self.url
@@ -828,10 +830,10 @@
                     return dct['092a']
                 if '090a' in dct:   # for films. FIXME, is this legit?
                     return dct['090a']
-                cn = ('%s %s' % (dct.get('050a', ''), 
+                cn = ('%s %s' % (dct.get('050a', ''),
                                  dct.get('050b', ''))).strip()
-		if len(cn) < 2:
-                	cn = ('%s %s' % (dct.get('092a', ''), 
+                if len(cn) < 2:
+                        cn = ('%s %s' % (dct.get('092a', ''),
                                  dct.get('092b', ''))).strip()
                 return cn
             except:
@@ -901,14 +903,12 @@
 #----------------------------------------------------------------------
 # Activate the local integration module.
 
-if hasattr(settings, 'INTEGRATION_MODULE'):
-    import conifer.syrup.integration
-    hooks = __import__(settings.INTEGRATION_MODULE, fromlist=[''])
-    for k,v in hooks.__dict__.items():
-        if callable(v):
-            setattr(conifer.syrup.integration, k, v)
+if hasattr(settings, 'INTEGRATION_CLASS'):
+    modname, klassname = settings.INTEGRATION_CLASS.rsplit('.', 1) # e.g. 'foo.bar.baz.MyClass'
+    mod = __import__(modname, fromlist=[''])
+    klass = getattr(mod, klassname)
+    initialize_hooks(klass())
 
-
 #-----------------------------------------------------------------------------
 # this can't be imported until Membership is defined...
 



More information about the open-ils-commits mailing list