From: gfawcett Date: Sun, 3 Apr 2011 00:38:02 +0000 (+0000) Subject: Now using classes, not modules, to implement local integrations. X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=4426f62ce8a05e28003197ee8873263e4b7af768;p=syrup%2Fmasslnc.git 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. git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@1303 6d9bc8c9-1ec2-4278-b937-99fde70a366f --- diff --git a/conifer/TODO b/conifer/TODO index bf7547c..1a1eb2e 100644 --- a/conifer/TODO +++ b/conifer/TODO @@ -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') diff --git a/conifer/integration/evergreen_site.py b/conifer/integration/evergreen_site.py new file mode 100644 index 0000000..7554bf7 --- /dev/null +++ b/conifer/integration/evergreen_site.py @@ -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. + """ diff --git a/conifer/integration/uwindsor.py b/conifer/integration/uwindsor.py index 3e5d28f..db5fd78 100644 --- a/conifer/integration/uwindsor.py +++ b/conifer/integration/uwindsor.py @@ -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 - - -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" - -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?') - - -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)), - ] - - -#-------------------------------------------------- -# ILS integration - -EG_BASE = 'http://%s/' % settings.EVERGREEN_GATEWAY_SERVER -initialize(EG_BASE) - - -# 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(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 _item_status(item.bib_id) - -CACHE_TIME = 300 - -@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()) - - # volume check - based on v.1, etc. in call number - voltest = re.search(r'\w*v\.\s?(\d+)', callnum) - - # attachment test - attachtest = re.search(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") +class UWindsorIntegration(EvergreenIntegration): - 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 + OSRF_CAT_SEARCH_ORG_UNIT = 106 - if not bringfw: - dueinfo = time.strftime(DUE_FORMAT,earliestdue) - callno = callnum + #--------------------------------------------------------------------------- + # proxy server integration - # 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 + ezproxy_service = ezproxy.EZProxyService( + settings.UWINDSOR_EZPROXY_HOST, + settings.UWINDSOR_EZPROXY_PASSWORD) - 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 proxify_url(self, url): """ - 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. + 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. """ - 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) - -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) + return self.ezproxy_service.proxify(url) -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 + #--------------------------------------------------------------------------- + # campus information -def decode_role(role): - if role == 'Instructor': - return 'INSTR' - else: - return 'STUDT' + 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 + + 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)), + ] -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. + 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) - 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 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 + + def _decode_role(self, role): + if role == 'Instructor': + return 'INSTR' + else: + return 'STUDT' + + 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. + filter = uwindsor_fuzzy_lookup.build_filter(query, include_students) + results = uwindsor_fuzzy_lookup.search(filter) -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 + 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 - return '%s-%s-%s' % (site.course.code.replace('-', ''), - section, - site.start_term.code) -#-------------------------------------------------- -# proxy server integration + 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 -ezproxy_service = ezproxy.EZProxyService( - settings.UWINDSOR_EZPROXY_HOST, - settings.UWINDSOR_EZPROXY_PASSWORD) + return '%s-%s-%s' % (site.course.code.replace('-', ''), + section, + site.start_term.code) -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) + #--------------------------------------------------------------------------- + # copyright/permissions + 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. + """ + # 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 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.") diff --git a/conifer/local_settings.py.example b/conifer/local_settings.py.example index ba85208..622cec4 100644 --- a/conifer/local_settings.py.example +++ b/conifer/local_settings.py.example @@ -63,10 +63,10 @@ Z3950_CONFIG = ('zed.concat.ca', 210, 'OWA') #OWA,OSUL,CONIFER # 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' diff --git a/conifer/plumbing/hooksystem.py b/conifer/plumbing/hooksystem.py index e90e52c..16b4281 100644 --- a/conifer/plumbing/hooksystem.py +++ b/conifer/plumbing/hooksystem.py @@ -1,9 +1,12 @@ -# 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 diff --git a/conifer/syrup/integration.py b/conifer/syrup/integration.py index 8c5161f..033a2ac 100644 --- a/conifer/syrup/integration.py +++ b/conifer/syrup/integration.py @@ -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 is here for documentation +# purposes only. You are not required to subclass Integration when you +# write your own integration code. -# Please do not define anything in this file. It will be automatically -# populated once confier.syrup.models has been evaluated. def disable(func): return None -@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. - """ - - -@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 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(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(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 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 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. - - 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 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. - """ - -@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 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). - """ - -@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. - """ - -@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. - """ - -@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. - """ - -@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. - """ +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. + """ + + @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 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). + + 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. + """ + + @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. + """ + + @disable + 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. + """ + + @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). + """ + + @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. + """ + + @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 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. + """ + + @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. + """ diff --git a/conifer/syrup/models.py b/conifer/syrup/models.py index e428595..0c83aa5 100644 --- a/conifer/syrup/models.py +++ b/conifer/syrup/models.py @@ -78,7 +78,7 @@ class UserExtensionMixin(object): 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 @@ class UserExtensionMixin(object): 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 @@ class UserExtensionMixin(object): 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 @@ class Site(BaseModel): """ 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 @@ class Site(BaseModel): 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 @@ class Group(BaseModel): # 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 @@ class Item(BaseModel): # 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 @@ class Item(BaseModel): ('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 @@ class Item(BaseModel): ('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 @@ class Item(BaseModel): ('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 @@ class Item(BaseModel): 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 @@ class Item(BaseModel): 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,13 +903,11 @@ def highlight(text, phrase, #---------------------------------------------------------------------- # 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...