[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