From d5f83dc5dd7e82e6d3e88d37860d52183a6b8480 Mon Sep 17 00:00:00 2001 From: gfawcett Date: Thu, 15 Jul 2010 00:55:26 +0000 Subject: [PATCH] moving to new integration system; dummy item_status integration. git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@918 6d9bc8c9-1ec2-4278-b937-99fde70a366f --- conifer/custom/lib_integration.py | 98 ------- conifer/integration/COURSE_CODES.txt | 140 ---------- conifer/integration/COURSE_SECTIONS.txt | 153 ---------- conifer/integration/_hooksystem.py | 29 +- conifer/integration/campus-interface.md | 338 ----------------------- conifer/integration/default.py | 18 -- conifer/integration/example.py | 72 ----- conifer/integration/hooks.py | 41 ++- conifer/libsystems/evergreen/item_status.py | 2 +- conifer/libsystems/z3950/marcxml.py | 22 +- conifer/libsystems/z3950/pyz3950_search.py | 3 +- conifer/static/main.css | 4 + conifer/syrup/integration.py | 71 +++++ conifer/syrup/models.py | 26 +- conifer/syrup/views/__init__.py | 1 - conifer/syrup/views/_common.py | 1 - conifer/syrup/views/items.py | 173 +----------- conifer/templates/components/site.xhtml | 16 ++ conifer/templates/item/item_add_cat_search.xhtml | 3 +- 19 files changed, 170 insertions(+), 1041 deletions(-) delete mode 100644 conifer/custom/lib_integration.py delete mode 100644 conifer/integration/COURSE_CODES.txt delete mode 100644 conifer/integration/COURSE_SECTIONS.txt delete mode 100644 conifer/integration/campus-interface.md delete mode 100644 conifer/integration/default.py delete mode 100644 conifer/integration/example.py create mode 100644 conifer/syrup/integration.py diff --git a/conifer/custom/lib_integration.py b/conifer/custom/lib_integration.py deleted file mode 100644 index 50686ee..0000000 --- a/conifer/custom/lib_integration.py +++ /dev/null @@ -1,98 +0,0 @@ -# Our integration-point with back-end library systems. - -# This is a work in progress. I'm trying to separate out the actual -# protocol handlers (in libsystems) from the configuration decicions -# (in settings.py), and use this as sort of a merge-point between -# those two decisions. - -# TODO: write some documentation about the lib_integration interface. - -# Our example configuration: -# Z39.50 for catalogue search, -# SIP for patron and item_info, and for item checkout and checkin, -# OpenSRF for extended item info. - -# define a @caching decorator to exploit the Django cache. Fixme, move -# this somewhere else. -from django.core.cache import cache -import cPickle -def caching(prefix, timeout=60): - def g(func): - def f(*args): - # wtf! Django encodes string-values as - # unicode-strings. That's bad, like stupid-bad! I'm - # putting explicit utf8-conversions here to make debugging - # easier if this code dies. - key = ','.join([prefix] + map(str, args)) - v = cache.get(key) - if v: - return cPickle.loads(v.encode('utf-8')) - else: - v = func(*args) - if v: - cache.set(key, unicode(cPickle.dumps(v), 'utf-8'), timeout) - return v - return f - return g - - -from django.conf import settings - -from conifer.libsystems.evergreen.support import initialize -EG_BASE = 'http://%s/' % settings.EVERGREEN_GATEWAY_SERVER -try: - initialize(EG_BASE) -except: - import warnings - warnings.warn('Evergreen inaccessible! Integration will suck eggs!') - -from conifer.libsystems.evergreen import item_status as I -from conifer.libsystems.sip.sipclient import SIP -#from conifer.libsystems.z3950 import yaz_search -from conifer.libsystems.z3950 import pyz3950_search -from conifer.libsystems.z3950.marcxml import marcxml_to_records - - -@caching('patroninfo', timeout=300) -@SIP -def patron_info(conn, barcode): - return conn.patron_info(barcode) - -@caching('itemstatus', timeout=300) -@SIP -def item_status(conn, barcode): - return conn.item_info(barcode) - -@SIP -def checkout(conn, patron_barcode, item_barcode): - return conn.checkout(patron_barcode, item_barcode, '') - -@SIP -def checkin(conn, item_barcode): - return conn.checkin(item_barcode, institution='', location='') - - -@caching('bcbi', timeout=3600) -def barcode_to_bib_id(barcode): - return I.barcode_to_bib_id(barcode) - -@caching('bccp', timeout=3600) -def barcode_to_copy(barcode): - return I.barcode_to_copy(barcode) - -@caching('bimx', timeout=3600) -def bib_id_to_marcxml(bib_id): - return I.bib_id_to_marcxml(bib_id) - - -def cat_search(query, start=1, limit=10): - # this is a total hack for conifer. If the query is a Conifer - # title-detail URL, then return just that one item. - if query.startswith(EG_BASE): - results = marcxml_to_records(I.url_to_marcxml(query)) - numhits = len(results) - else: - cat_host, cat_port, cat_db = settings.Z3950_CONFIG - results, numhits = pyz3950_search.search(cat_host, cat_port, cat_db, query, start, limit) - #results, numhits = yaz_search.search(cat_host, cat_db, query, start, limit) - return results, numhits diff --git a/conifer/integration/COURSE_CODES.txt b/conifer/integration/COURSE_CODES.txt deleted file mode 100644 index 7482ad0..0000000 --- a/conifer/integration/COURSE_CODES.txt +++ /dev/null @@ -1,140 +0,0 @@ -# Validation and lookup of course codes. - -# This modules specifies an "course-code interface" and a null -# implementation of that interface. If your local system has rules for -# valid course codes, and a mechanism for looking up details of these -# codes, you can implement the interface according to your local -# rules. - - -# ------------------------------------------------------------ -# Overview and definitions - -# A course code identifies a specific course offering. Course codes -# map 1:N onto formal course titles: by looking up a code, we can -# derive a formal title (in theory, though it may not be possible for -# external reasons). - -# A course code is insufficient to specify a class list: we need a -# course section for that. A section ties a course code and term to an -# instructor(s) and a list of students. - -# Course codes may have cross-listings, i.e., other codes which refer -# to the same course, but which appear under a different department -# for various academic purposes. In our system, we make no attempt to -# subordinate cross-listings to a "primary" course code. - - -#------------------------------------------------------------ -# Notes on the interface -# -# The `course_code_is_valid` function will be used ONLY if -# course_code_list() returns None (it is a null implementation). If a -# course-list is available, the system will use a membership test for -# course-code validity. -# -# `course_code_lookup_title` will be used ONLY if `course_code_list` -# is implemented. -# -# -# "types" of the interface members -# -# course_code_is_valid (string) --> boolean. -# course_code_example : a string constant. -# course_code_list () --> list of strings -# course_code_lookup_title (string) --> string, or None. -# course_code_cross_listings (string) --> list of strings -# -# For each member, you MUST provide either a valid implementation, or -# set the member to None. See the null implementation below. - -#------------------------------------------------------------ -# Implementations - -# ------------------------------------------------------------ -# Here is a 'null implementation' of the course-code interface. No -# validation is done, nor are lookups. -# -# course_code_is_valid = None # anything is OK; -# course_code_example = None # no examples; -# course_code_lookup_title = None # no codes to list; -# course_code_cross_listings = None # no cross lists. - -# ------------------------------------------------------------ -# This one specifies a valid course-code format using a regular -# expression, and offers some example codes, but does not have a -# lookup system. -# -# import re -# -# def course_code_is_valid(course_code): -# pattern = re.compile(r'^\d{2}-\d{3}$') -# return bool(pattern.match(course_code)) -# -# course_code_example = '55-203; 99-105' -# -# course_code_list = None -# course_code_lookup_title = None -# course_code_cross_listings = None - - - -# ------------------------------------------------------------ -# This is a complete implementation, based on a hard-coded list of -# course codes and titles, and two cross-listed course codes. -# -# _codes = [('ENG100', 'Introduction to English'), -# ('ART108', 'English: An Introduction'), -# ('FRE238', 'Modern French Literature'), -# ('WEB203', 'Advanced Web Design'),] -# -# _crosslists = set(['ENG100', 'ART108']) -# -# course_code_is_valid = None -# course_code_example = 'ENG100; FRE238' -# -# def course_code_list(): -# return [a for (a,b) in _codes] -# -# def course_code_lookup_title(course_code): -# return dict(_codes).get(course_code) -# -# def course_code_cross_listings(course_code): -# if course_code in _crosslists: -# return list(_crosslists - set([course_code])) - - -# ------------------------------------------------------------ -# Provide your own implementation below. - - -#_codes = [('ENG100', 'Introduction to English'), -# ('ART108', 'English: An Introduction'), -# ('FRE238', 'Modern French Literature'), -# ('LIB201', 'Intro to Library Science'), -# ('WEB203', 'Advanced Web Design'),] - -_codes = [('ART99-100', 'Art History'), - ('BIOL55-350', 'Molecular Cell Biology'), - ('CRIM48-567', 'Current Issues in Criminology'), - ('ENGL26-280', 'Contemporary Literary Theory'), - ('ENGL26-420', 'Word and Image: The Contemporary Graphic Novel'), - ('SOCWK47-457', 'Advanced Social Work Research'),] - -_crosslists = set(['ENGL26-280', 'ENGL26-420']) - - -course_code_is_valid = None - -course_code_example = 'BIOL55-350; SOCWK47-457' - -def course_code_list(): - return [a for (a,b) in _codes] - -def course_code_lookup_title(course_code): - return dict(_codes).get(course_code) - -def course_code_cross_listings(course_code): - if course_code in _crosslists: - return list(_crosslists - set([course_code])) - diff --git a/conifer/integration/COURSE_SECTIONS.txt b/conifer/integration/COURSE_SECTIONS.txt deleted file mode 100644 index 4003e0b..0000000 --- a/conifer/integration/COURSE_SECTIONS.txt +++ /dev/null @@ -1,153 +0,0 @@ -# Operations on course-section identifiers - -# A course section is an instance of a course offered in a term. - -# A section is specified by a 'section-id', a 3-tuple (course-code, -# term, section-code), where section-code is usually a short -# identifier (e.g., "1" representing "section 1 in this term"). Note -# that multiple sections of the same course are possible in a given -# term. - -# Within the reserves system, a course-site can be associated with -# zero or more sections, granting access to students in those -# sections. We need two representations of a section-id. - -# The section_tuple_delimiter must be a string which will never appear -# in a course-code, term, or section-code in your database. It may be -# a nonprintable character (e.g. NUL or CR). It is used to delimit -# parts of the tuples in a course's database record. - -#------------------------------------------------------------ -# Notes on the interface -# -# 'sections_taught_by(username)' returns a set of sections for which -# username is an instructor. It is acceptable if 'sections_taught_by' -# only returns current and future sections: historical information is -# not required by the reserves system. -# -# It is expected that the reserves system will be able to resolve any -# usernames into user records. If there are students on a section-list -# which do not resolve into user accounts, they will probably be -# ignored and will not get access to their course sites. So if you're -# updating your users and sections in a batch-run, you might want to -# update your users first. -# -#------------------------------------------------------------ -# Implementations - -# The reserves system will work with a null-implementation of the -# course-section interface, but tasks related to course-sections will -# be unavailable. - -# ------------------------------------------------------------ -# The null implementation: -# -# sections_tuple_delimiter = None -# sections_taught_by = None -# students_in = None -# instructors_in = None -# sections_for_code_and_term = None - -# ------------------------------------------------------------ -# -# The minimal non-null implementation. At the least you must provide -# sections_tuple_delimiter and students_in. Lookups for instructors -# may be skipped. Note that sections passed to students_in are -# (term, course-code, section-code) tuples (string, string, string). -# -# sections_tuple_delimiter = '|' -# -# def students_in(*sections): -# ... -# return set_of_usernames -# -# instructors_in = None -# sections_for_code_and_term = None - -# ------------------------------------------------------------ -# A complete implementation, with a static database. - -# sections_tuple_delimiter = '|' -# -# _db = [ -# ('fred', ('2009W', 'ENG203', '1'), 'jim joe jack ellen ed'), -# ('fred', ('2009W', 'ENG327', '1'), 'ed paul bill'), -# ('bill', ('2009S', 'BIO323', '1'), 'alan june jack'), -# ('bill', ('2009S', 'BIO323', '2'), 'emmet'), -# ] -# -# def sections_taught_by(username): -# return set([s[1] for s in _db if s[0] == username]) -# -# def students_in(*sections): -# def inner(): -# for instr, sec, studs in _db: -# if sec in sections: -# for s in studs.split(' '): -# yield s -# return set(inner()) -# -# def instructors_in(*sections): -# def inner(): -# for instr, sec, studs in _db: -# if sec in sections: -# yield instr -# return set(inner()) -# -# def sections_for_code_and_term(code, term): -# return [(t, c, s) for (instr, (t, c, s), ss) in _db \ -# if c == code and t == term] -# - - -# ------------------------------------------------------------ -# Provide your own implementation below. - -sections_tuple_delimiter = None -sections_taught_by = None -students_in = None -instructors_in = None -sections_for_code_and_term = None - - - -# ------------------------------------------------------------ -# a temporary implementation, while I write up the UI. - -sections_tuple_delimiter = '|' - -# For any of the students to actually appear in a course site, they -# must also exist as Django users (or be in an authentication backend -# that supports 'maybe_initialize_user'; see auth_evergreen.py). - -_db = [ - #(instructor, (term, code, sec-code), 'student1 student2 ... studentN'), - ('fred', ('2009W', 'ENG203', '1'), 'jim joe jack ellen ed'), - ('fred', ('2009W', 'ENG327', '1'), 'ed paul bill'), - ('art', ('2009W', 'LIB201', '1'), 'graham bill ed'), - ('graham', ('2009S', 'ART108', '1'), 'alan june jack'), - ('graham', ('2009S', 'ART108', '2'), 'emmet'), - ('graham', ('2009S', 'ART108', '3'), 'freda hugo bill'), -] - -def sections_taught_by(username): - return set([s[1] for s in _db if s[0] == username]) - -def students_in(*sections): - def inner(): - for instr, sec, studs in _db: - if sec in sections: - for s in studs.split(' '): - yield s - return set(inner()) - -def instructors_in(*sections): - def inner(): - for instr, sec, studs in _db: - if sec in sections: - yield instr - return set(inner()) - -def sections_for_code_and_term(code, term): - return [(t, c, s) for (instr, (t, c, s), ss) in _db \ - if c == code and t == term] diff --git a/conifer/integration/_hooksystem.py b/conifer/integration/_hooksystem.py index 43cb9ef..5144499 100644 --- a/conifer/integration/_hooksystem.py +++ b/conifer/integration/_hooksystem.py @@ -1,36 +1,21 @@ # TODO: decide whether or not to use this! import warnings +import conifer.syrup.integration as HOOKS -__all__ = ['hook', 'callhook', 'callhook_required', 'gethook'] - -__HOOKS = {} - -def __register_hook(name, func): - assert isinstance(name, basestring) - assert callable(func) - if name in __HOOKS: - warnings.warn('redefining hook %r (%r)' % (name, func)) - __HOOKS[name] = func - return func - -def hook(*args): - if isinstance(args[0], basestring): - return lambda f: __register_hook(args[0], f) - else: - f = args[0] - return __register_hook(f.__name__, f) +__all__ = ['callhook', 'callhook_required', 'gethook'] def gethook(name, default=None): - return __HOOKS.get(name, default) + print dir(HOOKS) + print (name, getattr(HOOKS, name)) + return getattr(HOOKS, name) or default def callhook_required(name, *args, **kwargs): - f = __HOOKS.get(name) + f = getattr(HOOKS, name) assert f, 'implementation for hook %r required but not found' % name return f(*args, **kwargs) def callhook(name, *args, **kwargs): - f = __HOOKS.get(name) + f = getattr(HOOKS, name) if f: return f(*args, **kwargs) - diff --git a/conifer/integration/campus-interface.md b/conifer/integration/campus-interface.md deleted file mode 100644 index e444e8e..0000000 --- a/conifer/integration/campus-interface.md +++ /dev/null @@ -1,338 +0,0 @@ -% DRAFT: Interface specification for an OpenSRF campus-information service -% Graham Fawcett -% March 26, 2010 - -# Introduction - -This document specifies the interface for an OpenSRF-based service -which gives OpenSRF applications access to *campus information*, such -as the names of courses taught at a given university, who is teaching -them, and which students are enrolled in them. - -This service is designed to meet the needs of our reserves -application, "Syrup." We hope the service will be useful in a wide -range of library applications that could benefit from access to course-related -information. - -This document specifies the OpenSRF interface of the campus -information service, but it does not dictate how the service must be -implemented. Each institution will need to implement the service, -using the tools of their choice, in a way that makes local sense. - -# Design considerations - -## Partial implementations - -In an ideal world, a library would have unlimited access to all the -course-related information they wanted; but many libraries do not -enjoy such access. Not all applications need the same types of -information, and many applications can adapt to different levels of -available campus information. Given this, it is acceptable to -*partially* implement the campus-information service, skipping the -parts that you cannot (or choose not to) implement. - -For example, if you don't have access to any class-list information, -but you do have a machine-readable version of the academic calendar, -you could implement the course-lookup and term-lookup parts of the -interface, but skip the course-offering parts. - -An application must be able to determine what parts of the interface -you have implemented. Therefore, you must implement the -`methods-supported` method (see [Static informational methods]). Since -this method-list is essentially static (it will change only if you -modify your implementation), an application may test it infrequently, -e.g. just once upon startup. - -## Caching - -OpenSRF provides a high-performance caching framework. You should -consider using this framework when designing your -implementation. - -Applications are discouraged from caching campus information: -especially information on people and course offerings, which both -change relatively frequently. It makes more sense to centralize policy -decisions about the lifespans of campus data at the service layer. If -applications must cache campus information (e.g. for demonstrated -performance reasons), they are encouraged to keep the cache-lifetimes -as short as possible. - -# Data types - -All of these data types are needed in a complete implementation of the -interface. Since you are free to implement only parts of the interface -(see [Partial implementations]), all of these data types might not -apply in your case. - -## Identifier types - - COURSE-ID = string (matching a local COURSE-ID-FORMAT) - TERM-ID = string (matching a local TERM-ID-FORMAT) - OFFERING-ID = string (matching a local OFFERING-ID-FORMAT) - PERSON-ID = string (matching a local PERSON-ID-FORMAT) - -The four identifier types are used respectively as unique keys for -courses, terms, course offerings, and people. (`String` is the -primitive type of strings of Unicode characers.) - -Since the PERSON-ID may be exposed in reports and user interfaces, it -should be a common public identifier (such as a 'single-sign-on ID', -'email prefix', or 'campus username') that can be displayed beside the -person's name without violating privacy regulations. - -Your institution may use 'section numbers' to differentiate multiple -offerings of a course in the same term. You may embed them in your -identifiers: for example, the offering ID `ENG100-2010W-03` might -represent Section 3 of English 100 being taught in Winter 2010. But it -isn't required that your offering IDs are so structured. - -**Formats:** Each type of identifier complies with a respective, -locally-defined format. You should define a (private, internal) -function for each format, that verifies whether a given string matches -the format. For example, a Java implementation might define a -function, `boolean isValidCourseID(String)`. You might use regular -expressions to define your formats, but it's not a requirement. At the -very least, your local formats should reject empty strings as IDs. You -may expose these functions for application use: see -[Format-matching methods]. - -## Record types - -Record types are modelled as associative arrays (sets of key/value -pairs). \[Is this acceptable in OpenSRF? It's valid JSON, but I'm not clear on OpenSRF conventions.\] -The following notation is used in the type definitions: - - string (a string primitive) - [string]* (an unordered set of zero or more strings) - (string)? (an optional string: it may be NULL.) - -Strictly, unordered sets *do* have an order, since they are -implemented as JSON lists. But the specification does not guarantee -that the order of the list is significant. - -Missing optional values may be indicated in two equivalent ways: -either include the key, and pair it with a `null` value (`{key: null, -...}`), or simply omit the key/value pair from the record. - - COURSE = { id: COURSE-ID, - title: string } - -A COURSE record describes a course in the abstract sense, as it would -appear in an academic calendar. It must have at least a unique course -ID and a descriptive (possibly non-unique) title. It may include other -attributes if you wish, but we specify `id` and `title` as required -attributes. - - TERM = { id: TERM-ID, - name: string, - start-date: date, - end-date: date } - -A TERM record describes a typical period in which a course is offered -(a 'term' or 'semester'). It must have a unique term-ID, a -probably-unique name, and start and end dates. (`Date` is a primitive -type, representing a calendar date.) - - PERSON = { id: PERSON-ID, - surname: string, - given-name: string, - email: (string)? } - -A PERSON record describes a person! It must include a unique -person-ID, a surname and given name. A value for `email` is -optional. You may also add other attributes as you see fit. - - OFFERING = { id: OFFERING-ID, - course: COURSE-ID, - starting-term: TERM-ID, - ending-term: TERM-ID, - students: [PERSON-ID]*, - assistants: [PERSON-ID]*, - instructors: [PERSON-ID]* } - -An OFFERING record describes a course offering: your institution might -call this a 'class' or a 'section'. It has specific start- and and -end-dates (derived from its starting and ending terms: some -institutions have courses that span multiple terms). The `course` -attribute refers to the single course of which it is an instance (our -specification punts on the issue of cross-listed offerings). It has -unordered sets of zero-or-more students, teaching assistants and -instructors. - -Each OFFERING record is a snapshot of a course offering at a given -time. It is assumed that people may join or leave the course -offering at any point during its duration. - -The set of "assistants" is loosely defined. It might include teaching -assistants (TAs and GAs) but also technical assistants, departmental -support staff, and other ancillary support staff. - - OFFERING-FLESHED = { id: OFFERING-ID, - course: COURSE, - starting-term: TERM, - ending-term: TERM, - students: [PERSON]*, - assistants: [PERSON]*, - instructors: [PERSON]* } - -A OFFERING-FLESHED record is like an OFFERING record, except that the -course, term, and people-set attributes have been 'fleshed out', so -that they contain not codes, but actual copies of the COURSE, TERM and -PERSON records. - -# Method signatures - -The following notation is used for method signatures: - - method-name: arg1-type, ... argN-type -> result-type - -The `void` type is used to express empty argument-lists. - -## Static informational methods - - methods-supported: void -> [string]* - -The `methods-supported` method is the only method that you *must* -implement (see [Partial implementations]). It returns a list of the -names of the methods for which you've provided -implementations. Applications can use this list to determine the -capabilities of your implementation. - -## Course methods - - course-lookup: COURSE-ID -> (COURSE)? - course-id-list: void -> [COURSE-ID]* - course-list: void -> [COURSE]* - course-id-example: void -> (COURSE-ID)? - -Given a COURSE-ID string, `course-lookup` will return the matching -COURSE record, or `null` if no such course exists. - -The methods `course-id-list` and `course-list` return a list of the -IDs (or records, respectively) of *all* known courses in the campus -system. An application might use these to populate option-lists or -report headings. The lists may be limited to the courses which are -defined in the current academic calendar (that is, your implementation -may omit obsolete course descriptions). - -The `course-id-example` method returns a course ID *example*. In -user-interfaces where a course ID must be typed in, this example can -be used to offer some guidance to the user. If the method returns -`null`, or if the method is not implemented, an application should -simply omit any example from the user interface. - -## Term methods - - term-lookup: TERM-ID -> (TERM)? - term-list: void -> [TERM]* - terms-at-date: date -> [TERM]* - -The `term-lookup` and `term-list` are analogous to the `course-lookup` -and `course-list` methods. The `terms-at-date` method takes a date -argument, and returns a list of all TERM records such that `term.start -<= date <= term.finish`. (We do not specify that terms are -non-overlapping.) - -## Person methods - - person-lookup: PERSON-ID -> (PERSON)? - -## Offering methods - -To describe the return-values of some of the Offering methods, we -introduce the notation `MBR(X)` as an abbreviation for the type -`([X]*, [X]*, [X]*)`, that is, a trio of sets representing the three -membership groups associated with a course offering: teachers, -assistants, and students. The types of elements contained in the sets -is specified by the specializing type, `X`: so, `MBR(PERSON)` is a -trio of sets of PERSON records. - - MBR(TYPE) = ([TYPE]*, # memberships as a teacher, - [TYPE]*, # as an assistant, - [TYPE]*) # as a student. - - - course-term-offerings: (COURSE-ID, TERM-ID) -> [OFFERING]* - course-term-offerings-fleshed: (COURSE-ID, TERM-ID) -> [OFFERING-FLESHED]* - -Given a COURSE-ID and a TERM-ID, these methods will return records for -all course offerings for the course represented by COURSE-ID, whose -`starting-term` *or* `ending-term` is equal to TERM-ID. - - memberships: PERSON-ID -> MBR(OFFERING) - membership-ids: PERSON-ID -> MBR(OFFERING-ID) - memberships-fleshed: PERSON-ID -> MBR(OFFERING-FLESHED) - -These methods take a PERSON-ID and return a trio of sets whose -elements represent the course-offerings in which the person is -(respectively) a teacher, assistant, or student. - -Within a given course-offering, a person must belong to no more than -one of the three sets. For example, it is not permitted to be both a -teacher and student for the same offering. - -If the PERSON-ID is invalid, or if the person is not a member of any -offerings, the return value should be a trio of three empty sets -- -`[[], [], []]` -- *not* a `null` value or an error. - - member-ids: OFFERING-ID -> MBR(PERSON-ID) - members: OFFERING-ID -> MBR(PERSON) - -These methods take an OFFERING-ID and return a trio of sets whose -elements represent (respectively) the teachers, assistants, and -students in the offering. - -If the OFFERING-ID is invalid, or if the offering is "empty", the -return value should be a trio of three empty sets -- `[[], [], []]` -- -*not* a `null` value or an error. - - teacher-ids: OFFERING-ID -> ([PERSON-ID]*, [PERSON-ID]*) - teachers: OFFERING-ID -> ([PERSON]*, [PERSON]*) - - -The `teacher` methods are identical to the `member` methods, except -that the student set is omitted: the return-value is a *pair* of sets -representing teachers and assistants. These are essentially optimized -versions of the `members` methods for cases when you only need to know -about the teaching and support teams (typically, very small groups) -and can avoid the cost of calculating and transmitting the student list -(typically, 10-100 times larger). - -## Format-matching methods - - resembles-course-id: string -> boolean - resembles-offering-id: string -> boolean - resembles-term-id: string -> boolean - resembles-person-id: string -> boolean - -Applications can use these to implement data-input validation tests in -user interfaces, primarily where lookups are not possible. They -determine whether a given string falls within the general guidelines -of what your IDs are supposed to look like. At some institutions, this -might be the best you can offer: you might not have access to -databases in which you can look records up, but at least you can offer -a means to avoid basic typographic errors. - -You could implement these methods by exposing the functions you -defined for your COURSE-ID-FORMAT, TERM-ID-FORMAT, OFFERING-ID-FORMAT -and PERSON-ID-FORMAT tests (see [Identifier types]). At the least, -these formats should ensure that empty strings are rejected. - -You might choose to use implement these as lookup functions, returning -`true` only if a matching record was found. For example, if your -school offers only two courses (say, `ENG100` and `ENG200`), you could -choose to implement a `resembles-course-id` method that only returned -`true` if the argument was exactly one of those two course codes. No -matter how you implement it, the intent of the `resembles` methods is -to help avoid typographic errors, not to act as a membership test. - -[Partial implementations]: #partial-implementations -[Static informational methods]: #static-informational-methods -[Identifier types]: #identifier-types -[Format-matching methods]: #format-matching-methods - - diff --git a/conifer/integration/default.py b/conifer/integration/default.py deleted file mode 100644 index 0d9a7b2..0000000 --- a/conifer/integration/default.py +++ /dev/null @@ -1,18 +0,0 @@ -# Do not edit this file: make your own, instead. - -# See COURSE_CODES.txt for information. - -course_code_is_valid = None -course_code_example = None -course_code_list = None -course_code_lookup_title = None -course_code_cross_listings = None - -# See COURSE_SECTIONS.txt for information. - -sections_tuple_delimiter = '|' -sections_taught_by = lambda u: [] -students_in = lambda *sections: set() -instructors_in = lambda *sections: set() -sections_for_code_and_term = lambda code, term: [] - diff --git a/conifer/integration/example.py b/conifer/integration/example.py deleted file mode 100644 index 2426949..0000000 --- a/conifer/integration/example.py +++ /dev/null @@ -1,72 +0,0 @@ -from default import * - -#---------------------------------------------------------------------- -# Course Codes - -_codes = [('ART99-100', 'Art History'), - ('BIOL55-350', 'Molecular Cell Biology'), - ('CRIM48-567', 'Current Issues in Criminology'), - ('ENGL26-280', 'Contemporary Literary Theory'), - ('ENGL26-420', 'Word and Image: The Contemporary Graphic Novel'), - ('SOCWK47-457', 'Advanced Social Work Research'),] - -_crosslists = set(['ENGL26-280', 'ENGL26-420']) - - -course_code_is_valid = None - -course_code_example = 'BIOL55-350; SOCWK47-457' - -def course_code_list(): - return [a for (a,b) in _codes] - -def course_code_lookup_title(course_code): - return dict(_codes).get(course_code) - -def course_code_cross_listings(course_code): - if course_code in _crosslists: - return list(_crosslists - set([course_code])) - - -#---------------------------------------------------------------------- -# Course Sections - -sections_tuple_delimiter = '|' - -# For any of the students to actually appear in a course site, they -# must also exist as Django users (or be in an authentication backend -# that supports 'maybe_initialize_user'; see auth_evergreen.py). - -_db = [ - #(instructor, (term, code, sec-code), 'student1 student2 ... studentN'), - ('fred', ('2009W', 'ENG203', '1'), 'jim joe jack ellen ed'), - ('fred', ('2009W', 'ENG327', '1'), 'ed paul bill'), - ('art', ('2009W', 'LIB201', '1'), 'graham bill ed'), - ('graham', ('2009S', 'ART108', '1'), 'alan june jack'), - ('graham', ('2009S', 'ART108', '2'), 'emmet'), - ('graham', ('2009S', 'ART108', '3'), 'freda hugo bill'), -] - -def sections_taught_by(username): - return set([s[1] for s in _db if s[0] == username]) - -def students_in(*sections): - def inner(): - for instr, sec, studs in _db: - if sec in sections: - for s in studs.split(' '): - yield s - return set(inner()) - -def instructors_in(*sections): - def inner(): - for instr, sec, studs in _db: - if sec in sections: - yield instr - return set(inner()) - -def sections_for_code_and_term(code, term): - return [(t, c, s) for (instr, (t, c, s), ss) in _db \ - if c == code and t == term] - - diff --git a/conifer/integration/hooks.py b/conifer/integration/hooks.py index 4ab4fe5..17fad18 100644 --- a/conifer/integration/hooks.py +++ b/conifer/integration/hooks.py @@ -1,16 +1,10 @@ -from conifer.integration._hooksystem import * from datetime import date +from django.conf import settings +from conifer.libsystems.evergreen.support import initialize +from conifer.libsystems.z3950 import marcxml as M +from conifer.libsystems.evergreen import item_status as I +from conifer.libsystems.z3950 import pyz3950_search as PZ -#---------------------------------------------------------------------- -# Your hooks go here. - -# @hook -# def can_create_sites(user): -# ... - -#TODO: this is for testing purposes only! Remove. - -@hook def department_course_catalogue(): """ Return a list of rows representing all known, active courses and @@ -28,7 +22,6 @@ def department_course_catalogue(): ('Social Work','02-47-456','Social Work and Health'), ] -@hook def term_catalogue(): """ Return a list of rows representing all known terms. Each row @@ -40,3 +33,27 @@ def term_catalogue(): ('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) + + +def item_status(item): + if 'psychology' in item.title.lower(): + return (8, 4, 2) + else: + return (2, 0, 0) + + +def cat_search(query, start=1, limit=10): + if query.startswith(EG_BASE): + results = M.marcxml_to_records(I.url_to_marcxml(query)) + numhits = len(results) + else: + cat_host, cat_port, cat_db = settings.Z3950_CONFIG + results, numhits = PZ.search(cat_host, cat_port, cat_db, query, start, limit) + return results, numhits diff --git a/conifer/libsystems/evergreen/item_status.py b/conifer/libsystems/evergreen/item_status.py index faa310c..315e735 100644 --- a/conifer/libsystems/evergreen/item_status.py +++ b/conifer/libsystems/evergreen/item_status.py @@ -27,7 +27,7 @@ def url_to_marcxml(url): if item_id: marc_url = ("%s/opac/extras/supercat/" "retrieve/marcxml/record/%s" % (support.BASE, item_id)) - xml = urllib2.urlopen(marc_url).read() + xml = unicode(urllib2.urlopen(marc_url).read(), 'utf-8') return xml if __name__ == '__main__': diff --git a/conifer/libsystems/z3950/marcxml.py b/conifer/libsystems/z3950/marcxml.py index 50a5f84..06ec460 100644 --- a/conifer/libsystems/z3950/marcxml.py +++ b/conifer/libsystems/z3950/marcxml.py @@ -1,11 +1,19 @@ from xml.etree import ElementTree # Note: the 'record' parameters passed to these functions must be -# Unicode strings, not plain Python strings. +# Unicode strings, not plain Python strings; or ElementTree instances. + +def _to_tree(unicode_or_etree): + if isinstance(unicode_or_etree, unicode): + tree = ElementTree.fromstring(unicode_or_etree.encode('utf-8')) + elif isinstance(unicode_or_etree, ElementTree._ElementInterface): + tree = unicode_or_etree + else: + raise Exception('Bad parameter', unicode_or_etree) + return tree def marcxml_to_records(rec): - assert isinstance(rec, unicode) - tree = ElementTree.fromstring(rec.encode('utf-8')) + tree = _to_tree(rec) if tree.tag == '{http://www.loc.gov/MARC21/slim}collection': # then we may have multiple records records = tree.findall('{http://www.loc.gov/MARC21/slim}record') @@ -15,9 +23,8 @@ def marcxml_to_records(rec): return [] return records -def record_to_dictionary(record, multiples=True): - assert isinstance(record, unicode) - tree = ElementTree.fromstring(record.encode('utf-8')) +def record_to_dictionary(rec, multiples=True): + tree = _to_tree(rec) dct = {} for df in tree.findall('{http://www.loc.gov/MARC21/slim}datafield'): t = df.attrib['tag'] @@ -29,8 +36,7 @@ def record_to_dictionary(record, multiples=True): return dct def marcxml_to_dictionary(rec, multiples=False): - assert isinstance(rec, unicode) - tree = ElementTree.fromstring(rec.encode('utf-8')) + tree = _to_tree(rec) if tree.tag == '{http://www.loc.gov/MARC21/slim}collection': # then we may have multiple records records = tree.findall('{http://www.loc.gov/MARC21/slim}record') diff --git a/conifer/libsystems/z3950/pyz3950_search.py b/conifer/libsystems/z3950/pyz3950_search.py index dbdbf7e..87abb63 100644 --- a/conifer/libsystems/z3950/pyz3950_search.py +++ b/conifer/libsystems/z3950/pyz3950_search.py @@ -8,6 +8,7 @@ import warnings import re import sys from marcxml import marcxml_to_dictionary +from xml.etree import ElementTree as ET try: @@ -77,7 +78,7 @@ def search(host, port, database, query, start=1, limit=10): rec = unicode(rec, 'ascii', 'replace') assert isinstance(rec, unicode) # this must be true. - parsed.append(rec) + parsed.append(ET.fromstring(rec.encode('utf-8'))) return parsed, len(res) diff --git a/conifer/static/main.css b/conifer/static/main.css index 0146aa7..0da63f1 100644 --- a/conifer/static/main.css +++ b/conifer/static/main.css @@ -343,3 +343,7 @@ ul.heading_tree li { list-style: none; } ul.heading_tree { margin: 0; padding-left: 0; } ul.heading_tree ul { margin: 0; padding-left: 25px; } + +.availability { float: right; color: darkred; background-color: #eee; padding: 4px; min-width: 24px; } +.availability .available { color: green; } +.avail_nonphys { background-color: white; } \ No newline at end of file diff --git a/conifer/syrup/integration.py b/conifer/syrup/integration.py new file mode 100644 index 0000000..5911138 --- /dev/null +++ b/conifer/syrup/integration.py @@ -0,0 +1,71 @@ +# this is a placeholder module, for the definitions in the +# INTEGRATION_MODULE defined in local_settings.py. + +# 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. + """ + pass + + +@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'). + """ + pass + + +@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. + """ + pass + + +@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. + """ diff --git a/conifer/syrup/models.py b/conifer/syrup/models.py index 929423b..2d525d1 100644 --- a/conifer/syrup/models.py +++ b/conifer/syrup/models.py @@ -13,9 +13,7 @@ from conifer.integration._hooksystem import * from django.conf import settings campus = settings.CAMPUS_INTEGRATION # TODO: fixme, not sure if conifer.custom is a good parent. -from conifer.custom import lib_integration -from conifer.libsystems.z3950.marcxml import record_to_dictionary -from conifer.libsystems.z3950.marcxml import marcxml_dictionary_to_dc +import conifer.libsystems.z3950.marcxml as MX from django.utils import simplejson as json #---------------------------------------------------------------------- @@ -470,7 +468,7 @@ class Item(BaseModel): #-------------------------------------------------- # MARC def marc_as_dict(self): - return record_to_dictionary(self.marcxml) + return MX.record_to_dictionary(self.marcxml) def marc_dc_subset(self): return json.dumps(self.marc_as_dict()) @@ -524,7 +522,15 @@ class Item(BaseModel): and a friendly description of the physical item's status""" # TODO: this needs to be reimplemented, based on copy detail # lookup in the ILS. It also may not belong here! - return (True, 'NOT-IMPLEMENTED') + #return (True, 'NOT-IMPLEMENTED') + stat = callhook('item_status', self) + if not stat: + return (False, 'Status information not available.') + else: + lib, desk, avail = stat + return (avail > 0, + '%d of %d copies available at reserves desk; %d total copies in library system' % ( + avail, desk, lib)) # TODO: stuff I'm not sure about yet. I don't think it belongs here. @@ -560,3 +566,13 @@ def highlight(text, phrase, return literal(highlight_re.sub(highlighter, text)) else: return highlight_re.sub(highlighter, text) + + + +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) + diff --git a/conifer/syrup/views/__init__.py b/conifer/syrup/views/__init__.py index e308320..2554718 100644 --- a/conifer/syrup/views/__init__.py +++ b/conifer/syrup/views/__init__.py @@ -1,4 +1,3 @@ -from conifer.integration import hooks from general import * from sites import * from items import * diff --git a/conifer/syrup/views/_common.py b/conifer/syrup/views/_common.py index 896ddc3..24bf849 100644 --- a/conifer/syrup/views/_common.py +++ b/conifer/syrup/views/_common.py @@ -19,7 +19,6 @@ import django.forms import re import sys from django.forms.models import modelformset_factory -from conifer.custom import lib_integration from conifer.libsystems.z3950.marcxml import (marcxml_to_dictionary, marcxml_dictionary_to_dc) from conifer.syrup.fuzzy_match import rank_pending_items diff --git a/conifer/syrup/views/items.py b/conifer/syrup/views/items.py index 148f390..378149e 100644 --- a/conifer/syrup/views/items.py +++ b/conifer/syrup/views/items.py @@ -1,6 +1,8 @@ from _common import * from django.utils.translation import ugettext as _ from xml.etree import ElementTree as E +from conifer.syrup import integration + @members_only def item_detail(request, site_id, item_id): @@ -150,7 +152,7 @@ def item_add_cat_search(request, site_id, item_id): site=site, parent_item=parent_item) query = request.GET.get('query','').strip() start, limit = (int(request.GET.get(k,v)) for k,v in (('start',1),('limit',10))) - results, numhits = lib_integration.cat_search(query, start, limit) + results, numhits = integration.cat_search(query, start, limit) return g.render('item/item_add_cat_search.xhtml', results=results, query=query, start=start, limit=limit, numhits=numhits, @@ -316,172 +318,3 @@ def item_relocate(request, site_id, item_id): return HttpResponseRedirect(new_parent.item_url('meta')) else: return HttpResponseRedirect(site.site_url()) - - - -#----------------------------------------------------------------------------- -# Physical item processing - -@admin_only # fixme, is this the right permission? -def phys_index(request): - return g.render('phys/index.xhtml') - -@admin_only # fixme, is this the right permission? -def phys_checkout(request): - if request.method != 'POST': - return g.render('phys/checkout.xhtml', step=1) - else: - post = lambda k: request.POST.get(k, '').strip() - # dispatch based on what 'step' we are at. - step = post('step') - func = {'1': _phys_checkout_get_patron, - '2':_phys_checkout_do_checkout, - '3':_phys_checkout_do_another, - }[step] - return func(request) - -def _phys_checkout_get_patron(request): - post = lambda k: request.POST.get(k, '').strip() - patron, item = post('patron'), post('item') - msg = lib_integration.patron_info(patron) - if not msg['success']: - return simple_message(_('Invalid patron barcode'), - _('No such patron could be found.')) - else: - patron_descrip = '%s (%s) — %s' % ( - msg['personal'], msg['home_library'], msg['screenmsg']) - return g.render('phys/checkout.xhtml', step=2, - patron=patron, patron_descrip=patron_descrip) - -def _phys_checkout_do_checkout(request): - post = lambda k: request.POST.get(k, '').strip() - patron, item = post('patron'), post('item') - patron_descrip = post('patron_descrip') - - # make sure the barcode actually matches with a known barcode in - # Syrup. We only checkout what we know about. - matches = models.Item.with_barcode(item) - if not matches: - is_successful = False - item_descrip = None - else: - msg_status = lib_integration.item_status(item) - msg_checkout = lib_integration.checkout(patron, item) - is_successful = msg_checkout['success'] - item_descrip = '%s — %s' % ( - msg_status['title'], msg_status['status']) - - # log the checkout attempt. - log_entry = models.CheckInOut.objects.create( - is_checkout = True, - is_successful = is_successful, - staff = request.user, - patron = patron, - patron_descrip = patron_descrip, - item = item, - item_descrip = item_descrip) - log_entry.save() - - if not matches: - return simple_message( - _('Item not found in Reserves'), - _('This item does not exist in the Reserves database! ' - 'Cannot check it out.')) - else: - return g.render('phys/checkout.xhtml', step=3, - patron=patron, item=item, - patron_descrip=patron_descrip, - checkout_result=msg_checkout, - item_descrip=item_descrip) - -def _phys_checkout_do_another(request): - post = lambda k: request.POST.get(k, '').strip() - patron = post('patron') - patron_descrip = post('patron_descrip') - return g.render('phys/checkout.xhtml', step=2, - patron=patron, - patron_descrip=patron_descrip) - -#------------------------------------------------------------ - -@admin_only -def phys_mark_arrived(request): - if request.method != 'POST': - return g.render('phys/mark_arrived.xhtml') - else: - barcode = request.POST.get('item', '').strip() - already = models.PhysicalObject.by_barcode(barcode) - if already: - msg = _('This item has already been marked as received. Date received: %s') - msg = msg % str(already.received) - return simple_message(_('Item already marked as received'), msg) - bib_id = lib_integration.barcode_to_bib_id(barcode) - if not bib_id: - return simple_message(_('Item not found'), - _('No item matching this barcode could be found.')) - - marcxml = lib_integration.bib_id_to_marcxml(bib_id) - dct = marcxml_to_dictionary(marcxml) - dublin = marcxml_dictionary_to_dc(dct) - # merge them - dct.update(dublin) - ranked = rank_pending_items(dct) - return g.render('phys/mark_arrived_choose.xhtml', - barcode=barcode, - bib_id=bib_id, - ranked=ranked, - metadata=dct) - -@admin_only -def phys_mark_arrived_match(request): - choices = [int(k.split('_')[1]) for k in request.POST if k.startswith('choose_')] - if not choices: - return simple_message(_('No matching items selected!'), - _('You must select one or more matching items from the list.')) - else: - barcode = request.POST.get('barcode', '').strip() - assert barcode - smallint = request.POST.get('smallint', '').strip() or None - try: - phys = models.PhysicalObject(barcode=barcode, - receiver = request.user, - smallint = smallint) - phys.save() - except Exception, e: - return simple_message(_('Error'), repr(e), go_back=True) - - for c in choices: - item = models.Item.objects.get(pk=c) - current_bc = item.barcode() - if current_bc: - item.metadata_set.filter(name='syrup:barcode').delete() - item.metadata_set.create(name='syrup:barcode', value=barcode) - item.save() - return g.render('phys/mark_arrived_outcome.xhtml') - -@admin_only -def phys_circlist(request): - term_code = request.GET.get('term') - if not term_code: - terms = models.Term.objects.order_by('code') - return g.render('phys/circlist_index.xhtml', terms=terms) - - term = get_object_or_404(models.Term, code=term_code) - - # gather the list of wanted items for this term. - # Fixme, I need a better way. - - cursor = django.db.connection.cursor() - q = "select item_id from syrup_metadata where name='syrup:barcode'" - cursor.execute(q) - bad_ids = set([r[0] for r in cursor.fetchall()]) - cursor.close() - - wanted = models.Item.objects.filter( - item_type='PHYS', site__term=term).select_related('metadata') - wanted = [w for w in wanted if w.id not in bad_ids] - return g.render('phys/circlist_for_term.xhtml', - term=term, - wanted=wanted) - - diff --git a/conifer/templates/components/site.xhtml b/conifer/templates/components/site.xhtml index 80382cd..3fbc49c 100644 --- a/conifer/templates/components/site.xhtml +++ b/conifer/templates/components/site.xhtml @@ -32,6 +32,22 @@ searchtext = _('search this site...') class="itemtree">
  • +
    + +
    + ${_avail}/${_desk} +
    +
    + ∅ +
    +
    +
    +
    ${item} diff --git a/conifer/templates/item/item_add_cat_search.xhtml b/conifer/templates/item/item_add_cat_search.xhtml index 48b06f7..6d08a82 100644 --- a/conifer/templates/item/item_add_cat_search.xhtml +++ b/conifer/templates/item/item_add_cat_search.xhtml @@ -1,5 +1,6 @@
    - +
    -- 2.11.0