From: gfawcett Date: Fri, 22 Jan 2010 02:48:02 +0000 (+0000) Subject: stripped out pesky Windows carriage returns X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=cde961e8d11b9a6d421e99a5fb572b281bfc061e;p=Syrup.git stripped out pesky Windows carriage returns git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@762 6d9bc8c9-1ec2-4278-b937-99fde70a366f --- diff --git a/conifer/README b/conifer/README index b0e94e9..f7656f1 100644 --- a/conifer/README +++ b/conifer/README @@ -1,99 +1,99 @@ -Syrup: A Reserves application ------------------------------- - -For more information, see -http://open-ils.org/dokuwiki/doku.php?id=scratchpad:reserves - -or contact -Art Rhyno -Graham Fawcett - - -State of the application ------------------------------- - -Coming along nicely, thank you! With a bit of patience, you ought to -be able to get a basic Syrup system running in no time. Integrating it -with your backend library and other systems will take longer, of -course. - -Required components ------------------------------- - -You need Python. Probably Python 2.5, I haven't tested with other -versions. You also need sqlite3 or another Django-compatible -database. Sqlite3 is recommended for kicking the tires, PostgreSQL for -production. - -Third-party Python dependencies: - - sudo easy_install Django Genshi Babel BabelDjango - -(You'll need 'setuptools' in order to have 'easy_install'.) - -Windows is very similar, see: - -http://groups.google.com/group/syrup-reserves-discuss/web/installing-syrup-in-windows - -Graham has the following versions installed. Not saying you need these -exact ones, just that they are known to work. - - Django-1.0.1_final-py2.5 - Babel-0.9.4-py2.5 - BabelDjango-0.2.2-py2.5 - Genshi-0.5.1-py2.5 - -Getting this thing to run ------------------------------- - -This might work: - -* Review settings.py. Maybe you need to edit stuff. Probably not for a - quick test. - -* ./manage.py syncdb - -Note: don't use the "./" syntax in windows for the commands, e.g.: - -C:\src\syrup\trunk\conifer>manage.py syncdb - -* During syncdb, create yourself a superuser account. - -* ./pybabel-extract (currently, this is optional) - -* ./manage.py runserver - -* visit http://localhost:8000/ and log in. - -* create at least one Term and one Department under Admin Options. - -* make yourself a course. - -* click on all of the links and see what they do. - - -Contents [out of date -- Ed.] ------------------------------- - -syrup/ -- the reserves app -middleware/ -- middleware component to integrate Genshi -locale/ -- the gettext files -templates/ -- the Genshi templates -static/ -- static JS, CSS, image files -doc/ -- documentation on the app - -local_settings.py.in -- a template for local_settings.py -genshi_support.py -- Genshi template integration -pybabel-extract -- a "make all" for the i18n files -babel.cfg -- Babel (i18n) configuration file - -The rest is straightforward Django stuff. - - -Customization ------------------------------- - -The 'custom' directory contains (or should contain!) all of the bits -that you really need to customize for your institution. More -documentation is needed here, but the source code is mostly +Syrup: A Reserves application +------------------------------ + +For more information, see +http://open-ils.org/dokuwiki/doku.php?id=scratchpad:reserves + +or contact +Art Rhyno +Graham Fawcett + + +State of the application +------------------------------ + +Coming along nicely, thank you! With a bit of patience, you ought to +be able to get a basic Syrup system running in no time. Integrating it +with your backend library and other systems will take longer, of +course. + +Required components +------------------------------ + +You need Python. Probably Python 2.5, I haven't tested with other +versions. You also need sqlite3 or another Django-compatible +database. Sqlite3 is recommended for kicking the tires, PostgreSQL for +production. + +Third-party Python dependencies: + + sudo easy_install Django Genshi Babel BabelDjango + +(You'll need 'setuptools' in order to have 'easy_install'.) + +Windows is very similar, see: + +http://groups.google.com/group/syrup-reserves-discuss/web/installing-syrup-in-windows + +Graham has the following versions installed. Not saying you need these +exact ones, just that they are known to work. + + Django-1.0.1_final-py2.5 + Babel-0.9.4-py2.5 + BabelDjango-0.2.2-py2.5 + Genshi-0.5.1-py2.5 + +Getting this thing to run +------------------------------ + +This might work: + +* Review settings.py. Maybe you need to edit stuff. Probably not for a + quick test. + +* ./manage.py syncdb + +Note: don't use the "./" syntax in windows for the commands, e.g.: + +C:\src\syrup\trunk\conifer>manage.py syncdb + +* During syncdb, create yourself a superuser account. + +* ./pybabel-extract (currently, this is optional) + +* ./manage.py runserver + +* visit http://localhost:8000/ and log in. + +* create at least one Term and one Department under Admin Options. + +* make yourself a course. + +* click on all of the links and see what they do. + + +Contents [out of date -- Ed.] +------------------------------ + +syrup/ -- the reserves app +middleware/ -- middleware component to integrate Genshi +locale/ -- the gettext files +templates/ -- the Genshi templates +static/ -- static JS, CSS, image files +doc/ -- documentation on the app + +local_settings.py.in -- a template for local_settings.py +genshi_support.py -- Genshi template integration +pybabel-extract -- a "make all" for the i18n files +babel.cfg -- Babel (i18n) configuration file + +The rest is straightforward Django stuff. + + +Customization +------------------------------ + +The 'custom' directory contains (or should contain!) all of the bits +that you really need to customize for your institution. More +documentation is needed here, but the source code is mostly well-documented. \ No newline at end of file diff --git a/conifer/custom/course_codes.py b/conifer/custom/course_codes.py index 76aa334..7482ad0 100644 --- a/conifer/custom/course_codes.py +++ b/conifer/custom/course_codes.py @@ -1,140 +1,140 @@ -# 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])) - +# 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/custom/lib_integration.py b/conifer/custom/lib_integration.py index b1061af..ebe08bf 100644 --- a/conifer/custom/lib_integration.py +++ b/conifer/custom/lib_integration.py @@ -1,98 +1,98 @@ -# 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_dictionary - - -@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_dictionary(I.url_to_marcxml(query), multiples=True) - 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 +# 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_dictionary + + +@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_dictionary(I.url_to_marcxml(query), multiples=True) + 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/libsystems/sip/sipclient.py b/conifer/libsystems/sip/sipclient.py index 4bce1f8..0b21960 100644 --- a/conifer/libsystems/sip/sipclient.py +++ b/conifer/libsystems/sip/sipclient.py @@ -1,569 +1,569 @@ -# Small portions are borrowed from David Fiander's acstest.py, in the -# openncip project. David's license is below: - -# Copyright (C) 2006-2008 Georgia Public Library Service -# -# Author: David J. Fiander -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of version 2 of the GNU General Public -# License as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public -# License along with this program; if not, write to the Free -# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, -# MA 02111-1307 USA - - -from sipconstants import * -import socket -import sys -from datetime import datetime -import re - -DEBUG = True - -# ------------------------------------------------------------ -# helper functions - -def split_n(n): - """Return a function that splits a string into two parts at index N.""" - return lambda s: (s[:n], s[n:]) - -split2 = split_n(2) - - - - -# ------------------------------------------------------------ -# Messages - -# First we build up a little language for defining SIP messages, so -# that we can define the protocol in a declarative style. - - -class basefield(object): - - def encode(self, dct): - """Take a dict, and return the wire representation of this field.""" - raise NotImplementedError, repr(self) - - def decode(self, bytes): - """ - Take a wire representation and return a pair (V,R) where V is - the translated value of the current field, and R is the - remaining bytes after the field has been read. If this is an - optional field, then decode should return None for V, and - return the input bytes for R. - """ - raise NotImplementedError, repr(self) - - -class field(basefield): - - def __init__(self, name, code, width=None): - self.name = name - self.code = code - self.width = None # don't use this yet. - - def encode(self, dct): - return '%s%s|' % (self.code, dct.get(self.name, '')) - - def decode(self, bytes): - bcode, rest = split2(bytes) - if bcode != self.code: - raise 'BadDecode', \ - 'Wrong field! Expected %r (%s) got %r (%s), in %r.' % ( - self.code, lookup_constant(self.code), - bcode, lookup_constant(bcode), - bytes) - data, rest = rest.split('|', 1) - return data, rest - - -class optfield(field): # an optional field - - def decode(self, bytes): - tmp = bytes + ' ' - bcode, rest = split2(tmp) - if bcode == self.code: - return field.decode(self, bytes) - else: - return None, bytes - - -class charfield(basefield): - - def __init__(self, name, width=None, default=None): - self.name = name - self.dflt = str(default) - self.width = width or len(self.dflt) # give at least one - self.pad = ' ' * self.width - - self.decode = split_n(self.width) - - def encode(self, dct): - v = dct.get(self.name, self.dflt) - assert v is not None - return ('%s%s' % (self.pad, v))[-self.width:] - - -class yn(basefield): - def __init__(self, name): - self.name = name - - def encode(self, dct): - return 'NY'[bool(dct.get(self.name))] - - def decode(self, bytes): - return (bytes[0] == 'Y'), bytes[1:] - - -class localtime(charfield): - def __init__(self, name): - self.name = name - self.width = 18 - - def encode(self, dct): - return datetime.now().strftime('%Y%m%d %H%M%S') - - def decode(self, bytes): - return split_n(self.width)(bytes) - -RAW = -55 -class raw(basefield): - name = 'raw' - # for debugging. - def decode(self, bytes): - return bytes, '\r' - -# We define a protocol Message as a list of fields. For now, -# message(A, B, C) is equivalent to the tuple (A,B,C). - -message = lambda *args: args - -# Encoding a message on to the wire. Args is a dict of field-values. - -def encode_msg(msg, args): - out = [] - add = out.append - for thing in msg: - if isinstance(thing, basefield): - add(thing.encode(args)) - else: - add(str(thing)) - return ''.join(out) - -# Decoding from the wire: - -def decode_msg(msg, bytes): - out = {} - add = out.__setitem__ - rest = bytes - - # Proper 'fields' have variable position in the tail of the - # message. So we treat them differently. - varposn = set([p for p in msg if isinstance(p, field)]) - varlookup = dict((x.code, x) for x in varposn) - fixedposn = [p for p in msg if not p in varposn] - - for part in fixedposn: - if isinstance(part, basefield): - good, rest = part.decode(rest) - if good is not None: - add(part.name, good) - else: - v = str(part) - good, rest = rest[:len(v)], rest[len(v):] - assert v == good - if DEBUG: print '%s == %r\n==== %r' % (getattr(part, 'name',''), good, rest) - - # Now we take what's left, chunk it, and try to resolve each one - # against a variable-position field. - segments = re.findall(r'(.*?\|)', rest) - - if DEBUG: print segments - - for segment in segments: - fld = varlookup.get(segment[:2]) - if fld: - good, rest = fld.decode(segment) - add(fld.name, good) - varposn.remove(fld) - else: - raise 'FieldNotProcessed: %s, %s' % (segment, lookup_constant(segment[:2])) - - # Let's make sure that any "required" fields were not missing. - notpresent = set(f for f in varposn if not isinstance(f, optfield)) - if notpresent: - for f in notpresent: - print 'MISSING: %-12s %s %s' % (f.name, f.code, lookup_constant(f.code)) - raise 'MandatoryFieldsNotPresent' - - return out - -# The SIP checksum. Borrowed from djfiander. - -def checksum(msg): - return '%04X' % ((0 - sum(map(ord, msg))) & 0xFFFF) - - -#------------------------------------------------------------ -# SIP Message Definitions - -# some common fields - - -fld_localtime = localtime('localtime') -fld_INST_ID = field('inst', FID_INST_ID) -fld_ITEM_ID = field('item', FID_ITEM_ID) -fld_PATRON_ID = field('patron', FID_PATRON_ID) -ofld_TERMINAL_PWD = optfield('termpwd', FID_TERMINAL_PWD) -fld_proto_version = charfield('version', default='2.00') -ofld_print_line = optfield('print_line', FID_PRINT_LINE) -ofld_screen_msg = optfield('screenmsg', FID_SCREEN_MSG) - -MESSAGES = { - LOGIN : message( - LOGIN, - '00', - field('uid', FID_LOGIN_UID), - field('pwd', FID_LOGIN_PWD), - field('locn', FID_LOCATION_CODE)), - - LOGIN_RESP : message( - LOGIN_RESP, - charfield('ok', width=1)), - - SC_STATUS : message( - SC_STATUS, - charfield('online', default='1'), - charfield('width', default='040'), - fld_proto_version), - - ACS_STATUS : message( - ACS_STATUS, - yn('online'), - yn('checkin_OK'), - yn('checkout_OK'), - yn('renewal_OK'), - yn('status_update_OK'), - yn('offline_OK'), - charfield('timeout', default='01'), - charfield('retries', default='9999'), - fld_localtime, - charfield('protocol', default='2.00'), - fld_INST_ID, - optfield('patron_id', FID_PATRON_ID), - optfield('item_id', FID_ITEM_ID), - optfield('terminal_pwd', FID_TERMINAL_PWD), - - optfield('instname', FID_LIBRARY_NAME), - field('supported', FID_SUPPORTED_MSGS), - optfield('ttylocn', FID_TERMINAL_LOCN), - ofld_screen_msg, - ofld_print_line), - PATRON_INFO : message( - PATRON_INFO, - charfield('lang', width=3, default=1), - fld_localtime, - charfield('holditemsreq', default='Y '), - fld_INST_ID, - fld_PATRON_ID, - ofld_TERMINAL_PWD, - optfield('patronpwd', FID_PATRON_PWD), - optfield('startitem', FID_START_ITEM, width=5), - optfield('enditem', FID_END_ITEM, width=5)), - - PATRON_INFO_RESP : message( - PATRON_INFO_RESP, - charfield('hmmm', width=14), - charfield('lang', width=3, default=1), - fld_localtime, - charfield('onhold', width=4), - charfield('overdue', width=4), - charfield('charged', width=4), - charfield('fine', width=4), - charfield('recall', width=4), - charfield('unavail_holds', width=4), - fld_INST_ID, - ofld_screen_msg, - ofld_print_line, - optfield('instname', FID_LIBRARY_NAME), - fld_PATRON_ID, - field('personal', FID_PERSONAL_NAME), - - optfield('hold_limit', FID_HOLD_ITEMS_LMT, width=4), - optfield('overdue_limit', FID_OVERDUE_ITEMS_LMT, width=4), - optfield('charged_limit', FID_OVERDUE_ITEMS_LMT, width=4), - - optfield('hold_items', FID_HOLD_ITEMS), - optfield('valid_patron_pwd', FID_VALID_PATRON_PWD), - - optfield('valid_patron', FID_VALID_PATRON), - optfield('currency', FID_CURRENCY), - optfield('fee_amt', FID_FEE_AMT), - optfield('fee_limit', FID_FEE_LMT), - optfield('home_addr', FID_HOME_ADDR), - optfield('email', FID_EMAIL), - optfield('home_phone', FID_HOME_PHONE), - optfield('patron_birthdate', FID_PATRON_BIRTHDATE), - optfield('patron_class', FID_PATRON_CLASS), - optfield('inet_profile', FID_INET_PROFILE), - optfield('home_library', FID_HOME_LIBRARY)), - - END_PATRON_SESSION : message( - END_PATRON_SESSION, - fld_localtime, - field('inst', FID_INST_ID), - field('patron', FID_PATRON_ID)), - - END_SESSION_RESP : message( - END_SESSION_RESP, - yn('session_ended'), - fld_localtime, - fld_INST_ID, - fld_PATRON_ID, - ofld_print_line, - ofld_screen_msg), - - CHECKOUT: message( - CHECKOUT, - yn('renewals_OK'), - yn('no_block'), - fld_localtime, - fld_localtime, - field('inst', FID_INST_ID), - field('patron', FID_PATRON_ID), - field('item', FID_ITEM_ID), - ), - - CHECKOUT_RESP: message( - CHECKOUT_RESP, - charfield('ok', width=1), - yn('is_renewal'), - yn('is_magnetic'), - yn('desensitize'), - fld_localtime, - field('inst', FID_INST_ID), - field('patron', FID_PATRON_ID), - field('item', FID_ITEM_ID), - field('due', FID_DUE_DATE), - field('title', FID_TITLE_ID), - optfield('media_type_code', FID_MEDIA_TYPE), - optfield('is_valid_patron', FID_VALID_PATRON), - ofld_print_line, - ofld_screen_msg), - - CHECKIN: message( - CHECKIN, - yn('is_retry'), - fld_localtime, - fld_localtime, - field('item', FID_ITEM_ID), - field('location', FID_CURRENT_LOCN), - field('inst', FID_INST_ID), - ofld_TERMINAL_PWD, - ), - - CHECKIN_RESP: message( - CHECKIN_RESP, - charfield('ok', width=1), - yn('resensitize'), - yn('is_magnetic'), - yn('alert'), - fld_localtime, - fld_INST_ID, - optfield('patron', FID_PATRON_ID), - field('item', FID_ITEM_ID), - field('title', FID_TITLE_ID), - optfield('media_type_code', FID_MEDIA_TYPE), - optfield('perm_locn', FID_PERM_LOCN), - optfield('due', FID_DUE_DATE), - ofld_print_line, - ofld_screen_msg, - ), -# yn('is_retry'), -# fld_localtime, -# fld_localtime, -# field('item', FID_ITEM_ID), -# field('location', FID_CURRENT_LOCN), -# ofld_TERMINAL_PWD, -# ), - - ITEM_INFORMATION : message( - ITEM_INFORMATION, - fld_localtime, - fld_INST_ID, - fld_ITEM_ID, - ofld_TERMINAL_PWD), - - ITEM_INFO_RESP : message( - ITEM_INFO_RESP, - charfield('circstat', width=2), - charfield('security', width=2), - charfield('feetype', width=2), - fld_localtime, - fld_ITEM_ID, - field('title', FID_TITLE_ID), - optfield('mediatype', FID_MEDIA_TYPE), - optfield('perm_locn', FID_PERM_LOCN), - optfield('current_locn', FID_CURRENT_LOCN), - optfield('item_props', FID_ITEM_PROPS), - optfield('currency', FID_CURRENCY), - optfield('fee', FID_FEE_AMT), - optfield('owner', FID_OWNER), - optfield('hold_queue_len', FID_HOLD_QUEUE_LEN), - optfield('due_date', FID_DUE_DATE), - - optfield('recall_date', FID_RECALL_DATE), - optfield('hold_pickup_date', FID_HOLD_PICKUP_DATE), - ofld_screen_msg, - ofld_print_line), - - RAW : message(raw()), -} - - -class SipClient(object): - def __init__(self, host, port, error_detect=False): - self.hostport = (host, port) - self.error_detect = error_detect - self.connect() - - def connect(self): - so = socket.socket() - so.connect(self.hostport) - self.socket = so - self.seqno = self.error_detect and 1 or 0 - - def close(self): - # fixme, do SIP close first. - self.socket.close() - - def send(self, outmsg, inmsg, args=None): - msg_template = MESSAGES[outmsg] - resp_template = MESSAGES[inmsg] - msg = encode_msg(msg_template, args or {}) - if self.error_detect: - # add the checksum - msg += 'AY%dAZ' % (self.seqno % 10) - self.seqno += 1 - msg += checksum(msg) - msg += '\r' - if DEBUG: print '>>> %r' % msg - self.socket.send(msg) - resp = self.socket.recv(1000) - if DEBUG: print '<<< %r' % resp - return decode_msg(resp_template, resp) - - - # -------------------------------------------------- - # Common protocol methods - - def login(self, uid, pwd, locn): - msg = self.send(LOGIN, LOGIN_RESP, - dict(uid=uid, pwd=pwd, locn=locn)) - return msg.get('ok') == '1' - - def status(self): - return self.send(SC_STATUS, ACS_STATUS) - - def patron_info(self, barcode): - msg = self.send(PATRON_INFO,PATRON_INFO_RESP, - {'patron':barcode, - 'startitem':1, 'enditem':2}) - # fixme, this may not be the best test of okayness - msg['success'] = msg.get('valid_patron') == 'Y' - return msg - - def checkout(self, patron, item, inst=''): - msg = self.send(CHECKOUT, CHECKOUT_RESP, - {'patron':patron, - 'inst': inst, - 'item':item}) - msg['media_type'] = MEDIA_TYPE_TABLE.get(msg.get('media_type_code')) - msg['success'] = msg.get('ok') == '1' - return msg - - def checkin(self, item, institution='', location=''): - msg = self.send(CHECKIN, CHECKIN_RESP, - {'inst': institution, - 'location':location, - 'is_retry':False, - 'item':item}) - msg['success'] = msg.get('ok') == '1' - return msg - - def item_info(self, barcode): - print("starting") - msg = self.send(ITEM_INFORMATION, ITEM_INFO_RESP, - {'item':barcode}) - print(msg['circstat']) - msg['available'] = msg['circstat'] == '03' - msg['status'] = ITEM_STATUS_TABLE[msg['circstat']] - return msg - - -# ------------------------------------------------------------ -# Django stuff. Optional. - -try: - from django.conf import settings - def sip_connection(): - sip = SipClient(*settings.SIP_HOST) - if not sip.login(*settings.SIP_CREDENTIALS): - raise 'SipLoginError' - return sip - - # decorator - def SIP(fn): - def f(*args, **kwargs): - conn = sip_connection() - resp = fn(conn, *args, **kwargs) - conn.close() - return resp - return f - -except ImportError: - pass - - -# ------------------------------------------------------------ -# Test code. - -if __name__ == '__main__': - from pprint import pprint - - sip = SipClient('comet.cs.uoguelph.ca', 8080) - resp = sip.login(uid='test', - pwd='test', locn='test') - pprint(resp) - pprint(sip.status()) - - pprint(sip.send(PATRON_INFO, PATRON_INFO_RESP, - {'patron':'scclient', - 'startitem':1, 'enditem':2})) - - # these are items from openncip's test database. - item_ids = ['1565921879', '0440242746', '660'] - bad_ids = ['xx' + i for i in item_ids] - for item in (item_ids + bad_ids): - result = sip.send(ITEM_INFORMATION, ITEM_INFO_RESP, - {'item':item}) - print '%-12s: %s' % (item, result['title'] or '????') - print sip.send(CHECKOUT, RAW, - {'patron':'scclient-2', - 'inst': 'UWOLS', - 'item':item}) - print '\n' * 5 - pprint(sip.send(END_PATRON_SESSION, END_SESSION_RESP, - {'patron':'scclient', - 'inst':'UWOLS'})) - - +# Small portions are borrowed from David Fiander's acstest.py, in the +# openncip project. David's license is below: + +# Copyright (C) 2006-2008 Georgia Public Library Service +# +# Author: David J. Fiander +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with this program; if not, write to the Free +# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA + + +from sipconstants import * +import socket +import sys +from datetime import datetime +import re + +DEBUG = True + +# ------------------------------------------------------------ +# helper functions + +def split_n(n): + """Return a function that splits a string into two parts at index N.""" + return lambda s: (s[:n], s[n:]) + +split2 = split_n(2) + + + + +# ------------------------------------------------------------ +# Messages + +# First we build up a little language for defining SIP messages, so +# that we can define the protocol in a declarative style. + + +class basefield(object): + + def encode(self, dct): + """Take a dict, and return the wire representation of this field.""" + raise NotImplementedError, repr(self) + + def decode(self, bytes): + """ + Take a wire representation and return a pair (V,R) where V is + the translated value of the current field, and R is the + remaining bytes after the field has been read. If this is an + optional field, then decode should return None for V, and + return the input bytes for R. + """ + raise NotImplementedError, repr(self) + + +class field(basefield): + + def __init__(self, name, code, width=None): + self.name = name + self.code = code + self.width = None # don't use this yet. + + def encode(self, dct): + return '%s%s|' % (self.code, dct.get(self.name, '')) + + def decode(self, bytes): + bcode, rest = split2(bytes) + if bcode != self.code: + raise 'BadDecode', \ + 'Wrong field! Expected %r (%s) got %r (%s), in %r.' % ( + self.code, lookup_constant(self.code), + bcode, lookup_constant(bcode), + bytes) + data, rest = rest.split('|', 1) + return data, rest + + +class optfield(field): # an optional field + + def decode(self, bytes): + tmp = bytes + ' ' + bcode, rest = split2(tmp) + if bcode == self.code: + return field.decode(self, bytes) + else: + return None, bytes + + +class charfield(basefield): + + def __init__(self, name, width=None, default=None): + self.name = name + self.dflt = str(default) + self.width = width or len(self.dflt) # give at least one + self.pad = ' ' * self.width + + self.decode = split_n(self.width) + + def encode(self, dct): + v = dct.get(self.name, self.dflt) + assert v is not None + return ('%s%s' % (self.pad, v))[-self.width:] + + +class yn(basefield): + def __init__(self, name): + self.name = name + + def encode(self, dct): + return 'NY'[bool(dct.get(self.name))] + + def decode(self, bytes): + return (bytes[0] == 'Y'), bytes[1:] + + +class localtime(charfield): + def __init__(self, name): + self.name = name + self.width = 18 + + def encode(self, dct): + return datetime.now().strftime('%Y%m%d %H%M%S') + + def decode(self, bytes): + return split_n(self.width)(bytes) + +RAW = -55 +class raw(basefield): + name = 'raw' + # for debugging. + def decode(self, bytes): + return bytes, '\r' + +# We define a protocol Message as a list of fields. For now, +# message(A, B, C) is equivalent to the tuple (A,B,C). + +message = lambda *args: args + +# Encoding a message on to the wire. Args is a dict of field-values. + +def encode_msg(msg, args): + out = [] + add = out.append + for thing in msg: + if isinstance(thing, basefield): + add(thing.encode(args)) + else: + add(str(thing)) + return ''.join(out) + +# Decoding from the wire: + +def decode_msg(msg, bytes): + out = {} + add = out.__setitem__ + rest = bytes + + # Proper 'fields' have variable position in the tail of the + # message. So we treat them differently. + varposn = set([p for p in msg if isinstance(p, field)]) + varlookup = dict((x.code, x) for x in varposn) + fixedposn = [p for p in msg if not p in varposn] + + for part in fixedposn: + if isinstance(part, basefield): + good, rest = part.decode(rest) + if good is not None: + add(part.name, good) + else: + v = str(part) + good, rest = rest[:len(v)], rest[len(v):] + assert v == good + if DEBUG: print '%s == %r\n==== %r' % (getattr(part, 'name',''), good, rest) + + # Now we take what's left, chunk it, and try to resolve each one + # against a variable-position field. + segments = re.findall(r'(.*?\|)', rest) + + if DEBUG: print segments + + for segment in segments: + fld = varlookup.get(segment[:2]) + if fld: + good, rest = fld.decode(segment) + add(fld.name, good) + varposn.remove(fld) + else: + raise 'FieldNotProcessed: %s, %s' % (segment, lookup_constant(segment[:2])) + + # Let's make sure that any "required" fields were not missing. + notpresent = set(f for f in varposn if not isinstance(f, optfield)) + if notpresent: + for f in notpresent: + print 'MISSING: %-12s %s %s' % (f.name, f.code, lookup_constant(f.code)) + raise 'MandatoryFieldsNotPresent' + + return out + +# The SIP checksum. Borrowed from djfiander. + +def checksum(msg): + return '%04X' % ((0 - sum(map(ord, msg))) & 0xFFFF) + + +#------------------------------------------------------------ +# SIP Message Definitions + +# some common fields + + +fld_localtime = localtime('localtime') +fld_INST_ID = field('inst', FID_INST_ID) +fld_ITEM_ID = field('item', FID_ITEM_ID) +fld_PATRON_ID = field('patron', FID_PATRON_ID) +ofld_TERMINAL_PWD = optfield('termpwd', FID_TERMINAL_PWD) +fld_proto_version = charfield('version', default='2.00') +ofld_print_line = optfield('print_line', FID_PRINT_LINE) +ofld_screen_msg = optfield('screenmsg', FID_SCREEN_MSG) + +MESSAGES = { + LOGIN : message( + LOGIN, + '00', + field('uid', FID_LOGIN_UID), + field('pwd', FID_LOGIN_PWD), + field('locn', FID_LOCATION_CODE)), + + LOGIN_RESP : message( + LOGIN_RESP, + charfield('ok', width=1)), + + SC_STATUS : message( + SC_STATUS, + charfield('online', default='1'), + charfield('width', default='040'), + fld_proto_version), + + ACS_STATUS : message( + ACS_STATUS, + yn('online'), + yn('checkin_OK'), + yn('checkout_OK'), + yn('renewal_OK'), + yn('status_update_OK'), + yn('offline_OK'), + charfield('timeout', default='01'), + charfield('retries', default='9999'), + fld_localtime, + charfield('protocol', default='2.00'), + fld_INST_ID, + optfield('patron_id', FID_PATRON_ID), + optfield('item_id', FID_ITEM_ID), + optfield('terminal_pwd', FID_TERMINAL_PWD), + + optfield('instname', FID_LIBRARY_NAME), + field('supported', FID_SUPPORTED_MSGS), + optfield('ttylocn', FID_TERMINAL_LOCN), + ofld_screen_msg, + ofld_print_line), + PATRON_INFO : message( + PATRON_INFO, + charfield('lang', width=3, default=1), + fld_localtime, + charfield('holditemsreq', default='Y '), + fld_INST_ID, + fld_PATRON_ID, + ofld_TERMINAL_PWD, + optfield('patronpwd', FID_PATRON_PWD), + optfield('startitem', FID_START_ITEM, width=5), + optfield('enditem', FID_END_ITEM, width=5)), + + PATRON_INFO_RESP : message( + PATRON_INFO_RESP, + charfield('hmmm', width=14), + charfield('lang', width=3, default=1), + fld_localtime, + charfield('onhold', width=4), + charfield('overdue', width=4), + charfield('charged', width=4), + charfield('fine', width=4), + charfield('recall', width=4), + charfield('unavail_holds', width=4), + fld_INST_ID, + ofld_screen_msg, + ofld_print_line, + optfield('instname', FID_LIBRARY_NAME), + fld_PATRON_ID, + field('personal', FID_PERSONAL_NAME), + + optfield('hold_limit', FID_HOLD_ITEMS_LMT, width=4), + optfield('overdue_limit', FID_OVERDUE_ITEMS_LMT, width=4), + optfield('charged_limit', FID_OVERDUE_ITEMS_LMT, width=4), + + optfield('hold_items', FID_HOLD_ITEMS), + optfield('valid_patron_pwd', FID_VALID_PATRON_PWD), + + optfield('valid_patron', FID_VALID_PATRON), + optfield('currency', FID_CURRENCY), + optfield('fee_amt', FID_FEE_AMT), + optfield('fee_limit', FID_FEE_LMT), + optfield('home_addr', FID_HOME_ADDR), + optfield('email', FID_EMAIL), + optfield('home_phone', FID_HOME_PHONE), + optfield('patron_birthdate', FID_PATRON_BIRTHDATE), + optfield('patron_class', FID_PATRON_CLASS), + optfield('inet_profile', FID_INET_PROFILE), + optfield('home_library', FID_HOME_LIBRARY)), + + END_PATRON_SESSION : message( + END_PATRON_SESSION, + fld_localtime, + field('inst', FID_INST_ID), + field('patron', FID_PATRON_ID)), + + END_SESSION_RESP : message( + END_SESSION_RESP, + yn('session_ended'), + fld_localtime, + fld_INST_ID, + fld_PATRON_ID, + ofld_print_line, + ofld_screen_msg), + + CHECKOUT: message( + CHECKOUT, + yn('renewals_OK'), + yn('no_block'), + fld_localtime, + fld_localtime, + field('inst', FID_INST_ID), + field('patron', FID_PATRON_ID), + field('item', FID_ITEM_ID), + ), + + CHECKOUT_RESP: message( + CHECKOUT_RESP, + charfield('ok', width=1), + yn('is_renewal'), + yn('is_magnetic'), + yn('desensitize'), + fld_localtime, + field('inst', FID_INST_ID), + field('patron', FID_PATRON_ID), + field('item', FID_ITEM_ID), + field('due', FID_DUE_DATE), + field('title', FID_TITLE_ID), + optfield('media_type_code', FID_MEDIA_TYPE), + optfield('is_valid_patron', FID_VALID_PATRON), + ofld_print_line, + ofld_screen_msg), + + CHECKIN: message( + CHECKIN, + yn('is_retry'), + fld_localtime, + fld_localtime, + field('item', FID_ITEM_ID), + field('location', FID_CURRENT_LOCN), + field('inst', FID_INST_ID), + ofld_TERMINAL_PWD, + ), + + CHECKIN_RESP: message( + CHECKIN_RESP, + charfield('ok', width=1), + yn('resensitize'), + yn('is_magnetic'), + yn('alert'), + fld_localtime, + fld_INST_ID, + optfield('patron', FID_PATRON_ID), + field('item', FID_ITEM_ID), + field('title', FID_TITLE_ID), + optfield('media_type_code', FID_MEDIA_TYPE), + optfield('perm_locn', FID_PERM_LOCN), + optfield('due', FID_DUE_DATE), + ofld_print_line, + ofld_screen_msg, + ), +# yn('is_retry'), +# fld_localtime, +# fld_localtime, +# field('item', FID_ITEM_ID), +# field('location', FID_CURRENT_LOCN), +# ofld_TERMINAL_PWD, +# ), + + ITEM_INFORMATION : message( + ITEM_INFORMATION, + fld_localtime, + fld_INST_ID, + fld_ITEM_ID, + ofld_TERMINAL_PWD), + + ITEM_INFO_RESP : message( + ITEM_INFO_RESP, + charfield('circstat', width=2), + charfield('security', width=2), + charfield('feetype', width=2), + fld_localtime, + fld_ITEM_ID, + field('title', FID_TITLE_ID), + optfield('mediatype', FID_MEDIA_TYPE), + optfield('perm_locn', FID_PERM_LOCN), + optfield('current_locn', FID_CURRENT_LOCN), + optfield('item_props', FID_ITEM_PROPS), + optfield('currency', FID_CURRENCY), + optfield('fee', FID_FEE_AMT), + optfield('owner', FID_OWNER), + optfield('hold_queue_len', FID_HOLD_QUEUE_LEN), + optfield('due_date', FID_DUE_DATE), + + optfield('recall_date', FID_RECALL_DATE), + optfield('hold_pickup_date', FID_HOLD_PICKUP_DATE), + ofld_screen_msg, + ofld_print_line), + + RAW : message(raw()), +} + + +class SipClient(object): + def __init__(self, host, port, error_detect=False): + self.hostport = (host, port) + self.error_detect = error_detect + self.connect() + + def connect(self): + so = socket.socket() + so.connect(self.hostport) + self.socket = so + self.seqno = self.error_detect and 1 or 0 + + def close(self): + # fixme, do SIP close first. + self.socket.close() + + def send(self, outmsg, inmsg, args=None): + msg_template = MESSAGES[outmsg] + resp_template = MESSAGES[inmsg] + msg = encode_msg(msg_template, args or {}) + if self.error_detect: + # add the checksum + msg += 'AY%dAZ' % (self.seqno % 10) + self.seqno += 1 + msg += checksum(msg) + msg += '\r' + if DEBUG: print '>>> %r' % msg + self.socket.send(msg) + resp = self.socket.recv(1000) + if DEBUG: print '<<< %r' % resp + return decode_msg(resp_template, resp) + + + # -------------------------------------------------- + # Common protocol methods + + def login(self, uid, pwd, locn): + msg = self.send(LOGIN, LOGIN_RESP, + dict(uid=uid, pwd=pwd, locn=locn)) + return msg.get('ok') == '1' + + def status(self): + return self.send(SC_STATUS, ACS_STATUS) + + def patron_info(self, barcode): + msg = self.send(PATRON_INFO,PATRON_INFO_RESP, + {'patron':barcode, + 'startitem':1, 'enditem':2}) + # fixme, this may not be the best test of okayness + msg['success'] = msg.get('valid_patron') == 'Y' + return msg + + def checkout(self, patron, item, inst=''): + msg = self.send(CHECKOUT, CHECKOUT_RESP, + {'patron':patron, + 'inst': inst, + 'item':item}) + msg['media_type'] = MEDIA_TYPE_TABLE.get(msg.get('media_type_code')) + msg['success'] = msg.get('ok') == '1' + return msg + + def checkin(self, item, institution='', location=''): + msg = self.send(CHECKIN, CHECKIN_RESP, + {'inst': institution, + 'location':location, + 'is_retry':False, + 'item':item}) + msg['success'] = msg.get('ok') == '1' + return msg + + def item_info(self, barcode): + print("starting") + msg = self.send(ITEM_INFORMATION, ITEM_INFO_RESP, + {'item':barcode}) + print(msg['circstat']) + msg['available'] = msg['circstat'] == '03' + msg['status'] = ITEM_STATUS_TABLE[msg['circstat']] + return msg + + +# ------------------------------------------------------------ +# Django stuff. Optional. + +try: + from django.conf import settings + def sip_connection(): + sip = SipClient(*settings.SIP_HOST) + if not sip.login(*settings.SIP_CREDENTIALS): + raise 'SipLoginError' + return sip + + # decorator + def SIP(fn): + def f(*args, **kwargs): + conn = sip_connection() + resp = fn(conn, *args, **kwargs) + conn.close() + return resp + return f + +except ImportError: + pass + + +# ------------------------------------------------------------ +# Test code. + +if __name__ == '__main__': + from pprint import pprint + + sip = SipClient('comet.cs.uoguelph.ca', 8080) + resp = sip.login(uid='test', + pwd='test', locn='test') + pprint(resp) + pprint(sip.status()) + + pprint(sip.send(PATRON_INFO, PATRON_INFO_RESP, + {'patron':'scclient', + 'startitem':1, 'enditem':2})) + + # these are items from openncip's test database. + item_ids = ['1565921879', '0440242746', '660'] + bad_ids = ['xx' + i for i in item_ids] + for item in (item_ids + bad_ids): + result = sip.send(ITEM_INFORMATION, ITEM_INFO_RESP, + {'item':item}) + print '%-12s: %s' % (item, result['title'] or '????') + print sip.send(CHECKOUT, RAW, + {'patron':'scclient-2', + 'inst': 'UWOLS', + 'item':item}) + print '\n' * 5 + pprint(sip.send(END_PATRON_SESSION, END_SESSION_RESP, + {'patron':'scclient', + 'inst':'UWOLS'})) + + diff --git a/conifer/libsystems/z3950/pyz3950_search.py b/conifer/libsystems/z3950/pyz3950_search.py index bd2bfc3..09a951f 100644 --- a/conifer/libsystems/z3950/pyz3950_search.py +++ b/conifer/libsystems/z3950/pyz3950_search.py @@ -1,101 +1,101 @@ -# z39.50 search using yaz-client. -# dependencies: yaz-client, pexpect - -# I found that pyz3950.zoom seemed wonky when testing against conifer -# z3950, so I whipped up this expect-based version instead. - -import warnings -import re -import sys -from marcxml import marcxml_to_dictionary - -try: - - import profile - import lex - import yacc -except ImportError: - - sys.modules['profile'] = sys # just get something called 'profile'; - # it's not actually used. - import ply.lex - import ply.yacc # pyz3950 thinks these are toplevel modules. - sys.modules['lex'] = ply.lex - sys.modules['yacc'] = ply.yacc - -# for Z39.50 support, not sure whether this is the way to go yet but -# as generic as it gets -from PyZ3950 import zoom, zmarc - - -LOG = None # for pexpect debugging, try LOG = sys.stderr -GENERAL_TIMEOUT = 40 -PRESENT_TIMEOUT = 60 - -def search(host, port, database, query, start=1, limit=10): - - - query = query.encode('utf-8') # is this okay? Is it enough?? - - conn = zoom.Connection(host, port) - conn.databaseName = database - conn.preferredRecordSyntax = 'XML' - - query = zoom.Query ('CCL', str(query)) - res = conn.search (query) - collector = [] - #if we were dealing with marc8 results, would probably need this - #m = zmarc.MARC8_to_Unicode () - - # how many to present? At most 10 for now. - to_show = min(len(res)-(start - 1), limit) - if limit: - to_show = min(to_show, limit) - - - #this seems to an efficient way of snagging the records - #would be good to cache the result set for iterative display - for r in range(start - 1,(start-1) + to_show): - #would need to translate marc8 records, evergreen doesn't need this - #collector.append(m.translate(r.data)) - collector.append(str(res.__getitem__(r)).replace('\n','')) - conn.close () - - - raw = "" . join(collector) - - raw_records = [] - err = None - - pat = re.compile('', re.M) - raw_records = pat.findall(raw) - - parsed = [] - for rec in raw_records: - try: - rec = _marc_utf8_pattern.sub(_decode_marc_utf8, rec) - dct = marcxml_to_dictionary(rec) - except 'x': - raise rec - parsed.append(dct) - return parsed, len(res) - - -# decoding MARC \X.. UTF-8 patterns. - -_marc_utf8_pattern = re.compile(r'\\X([0-9A-F]{2})') - -def _decode_marc_utf8(regex_match): - return chr(int(regex_match.group(1), 16)) - - -#------------------------------------------------------------ -# some tests - -if __name__ == '__main__': - tests = [ - ('zed.concat.ca:210', 'OSUL', 'chanson'), - ] - for host, db, query in tests: - print (host, db, query) - print len(search(host, db, query, limit=33)) +# z39.50 search using yaz-client. +# dependencies: yaz-client, pexpect + +# I found that pyz3950.zoom seemed wonky when testing against conifer +# z3950, so I whipped up this expect-based version instead. + +import warnings +import re +import sys +from marcxml import marcxml_to_dictionary + +try: + + import profile + import lex + import yacc +except ImportError: + + sys.modules['profile'] = sys # just get something called 'profile'; + # it's not actually used. + import ply.lex + import ply.yacc # pyz3950 thinks these are toplevel modules. + sys.modules['lex'] = ply.lex + sys.modules['yacc'] = ply.yacc + +# for Z39.50 support, not sure whether this is the way to go yet but +# as generic as it gets +from PyZ3950 import zoom, zmarc + + +LOG = None # for pexpect debugging, try LOG = sys.stderr +GENERAL_TIMEOUT = 40 +PRESENT_TIMEOUT = 60 + +def search(host, port, database, query, start=1, limit=10): + + + query = query.encode('utf-8') # is this okay? Is it enough?? + + conn = zoom.Connection(host, port) + conn.databaseName = database + conn.preferredRecordSyntax = 'XML' + + query = zoom.Query ('CCL', str(query)) + res = conn.search (query) + collector = [] + #if we were dealing with marc8 results, would probably need this + #m = zmarc.MARC8_to_Unicode () + + # how many to present? At most 10 for now. + to_show = min(len(res)-(start - 1), limit) + if limit: + to_show = min(to_show, limit) + + + #this seems to an efficient way of snagging the records + #would be good to cache the result set for iterative display + for r in range(start - 1,(start-1) + to_show): + #would need to translate marc8 records, evergreen doesn't need this + #collector.append(m.translate(r.data)) + collector.append(str(res.__getitem__(r)).replace('\n','')) + conn.close () + + + raw = "" . join(collector) + + raw_records = [] + err = None + + pat = re.compile('', re.M) + raw_records = pat.findall(raw) + + parsed = [] + for rec in raw_records: + try: + rec = _marc_utf8_pattern.sub(_decode_marc_utf8, rec) + dct = marcxml_to_dictionary(rec) + except 'x': + raise rec + parsed.append(dct) + return parsed, len(res) + + +# decoding MARC \X.. UTF-8 patterns. + +_marc_utf8_pattern = re.compile(r'\\X([0-9A-F]{2})') + +def _decode_marc_utf8(regex_match): + return chr(int(regex_match.group(1), 16)) + + +#------------------------------------------------------------ +# some tests + +if __name__ == '__main__': + tests = [ + ('zed.concat.ca:210', 'OSUL', 'chanson'), + ] + for host, db, query in tests: + print (host, db, query) + print len(search(host, db, query, limit=33)) diff --git a/conifer/settings.py b/conifer/settings.py index 7682924..439d05a 100644 --- a/conifer/settings.py +++ b/conifer/settings.py @@ -1,121 +1,121 @@ -# Django settings for conifer project. - -import os - -os.environ['PYTHON_EGG_CACHE'] = '/tmp/eggs' - -BASE_DIRECTORY = os.path.abspath(os.path.dirname(__file__)) -HERE = lambda s: os.path.join(BASE_DIRECTORY, s) - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - # ('Your Name', 'your_email@domain.com'), -) - -MANAGERS = ADMINS - -DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. -DATABASE_NAME = HERE('syrup.sqlite') # Or path to database file if using sqlite3. -DATABASE_USER = '' # Not used with sqlite3. -DATABASE_PASSWORD = '' # Not used with sqlite3. -DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. -DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'America/Detroit' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en_US' - -# Please only include languages here for which we have a locale in our -# locale/ directory. -LANGUAGES = [("en-us", "English"), - ("fr-ca", "Canadian French"), - ] - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# Absolute path to the directory that holds media. -# Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = HERE('static') - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash if there is a path component (optional in other cases). -# Examples: "http://media.lawrence.com", "http://example.com/media/" -MEDIA_URL = '' - -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/syrup/djmedia/' - -# Make this unique, and don't share it with anybody. -SECRET_KEY = 'j$dnxqbi3iih+(@il3m@vv(tuvt2+yu2r-$dxs$s7=iqjz_s!&' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'conifer.middleware.genshi_locals.ThreadLocals', - 'django.middleware.locale.LocaleMiddleware', - 'babeldjango.middleware.LocaleMiddleware', - # TransactionMiddleware should be last... - 'django.middleware.transaction.TransactionMiddleware', -) - -ROOT_URLCONF = 'conifer.urls' - -TEMPLATE_DIRS = [] - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.admin', - 'conifer.syrup', -) - -AUTH_PROFILE_MODULE = 'syrup.UserProfile' - - -AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - # uncomment for EG authentication: - #'conifer.custom.auth_evergreen.EvergreenAuthBackend', -) - - -EVERGREEN_GATEWAY_SERVER = 'www.concat.ca' -Z3950_CONFIG = ('zed.concat.ca', 210, 'OWA') #OWA,OSUL,CONIFER -SIP_HOST = ('localhost', 8080) - -try: - from private_local_settings import SIP_CREDENTIALS -except: - # stuff that I really ought not check into svn... - SIP_CREDENTIALS = ('test', 'test', 'test') - pass - - -#CACHE_BACKEND = 'memcached://127.0.0.1:11211/' -#CACHE_BACKEND = 'db://test_cache_table' -#CACHE_BACKEND = 'locmem:///' +# Django settings for conifer project. + +import os + +os.environ['PYTHON_EGG_CACHE'] = '/tmp/eggs' + +BASE_DIRECTORY = os.path.abspath(os.path.dirname(__file__)) +HERE = lambda s: os.path.join(BASE_DIRECTORY, s) + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +DATABASE_NAME = HERE('syrup.sqlite') # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Detroit' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en_US' + +# Please only include languages here for which we have a locale in our +# locale/ directory. +LANGUAGES = [("en-us", "English"), + ("fr-ca", "Canadian French"), + ] + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = HERE('static') + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/syrup/djmedia/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'j$dnxqbi3iih+(@il3m@vv(tuvt2+yu2r-$dxs$s7=iqjz_s!&' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'conifer.middleware.genshi_locals.ThreadLocals', + 'django.middleware.locale.LocaleMiddleware', + 'babeldjango.middleware.LocaleMiddleware', + # TransactionMiddleware should be last... + 'django.middleware.transaction.TransactionMiddleware', +) + +ROOT_URLCONF = 'conifer.urls' + +TEMPLATE_DIRS = [] + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.admin', + 'conifer.syrup', +) + +AUTH_PROFILE_MODULE = 'syrup.UserProfile' + + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + # uncomment for EG authentication: + #'conifer.custom.auth_evergreen.EvergreenAuthBackend', +) + + +EVERGREEN_GATEWAY_SERVER = 'www.concat.ca' +Z3950_CONFIG = ('zed.concat.ca', 210, 'OWA') #OWA,OSUL,CONIFER +SIP_HOST = ('localhost', 8080) + +try: + from private_local_settings import SIP_CREDENTIALS +except: + # stuff that I really ought not check into svn... + SIP_CREDENTIALS = ('test', 'test', 'test') + pass + + +#CACHE_BACKEND = 'memcached://127.0.0.1:11211/' +#CACHE_BACKEND = 'db://test_cache_table' +#CACHE_BACKEND = 'locmem:///' diff --git a/conifer/static/edit_course.js b/conifer/static/edit_course.js index 21bb4d1..8373aaf 100644 --- a/conifer/static/edit_course.js +++ b/conifer/static/edit_course.js @@ -1,19 +1,19 @@ -/* -this seems to be causing a disable when we don't want it -*/ -function do_init() { - if ($('#id_code')[0].tagName == 'SELECT') { - // code is a SELECT, so we add a callback to lookup titles. - $('#id_code').change(function() { - $('#id_title')[0].disabled=true; - $.getJSON('/syrup/course/new/ajax_title', {course_code: $(this).val()}, - function(resp) { - $('#id_title').val(resp.title) - $('#id_title')[0].disabled=false; - - }); - }); - } -} - -$(do_init); +/* +this seems to be causing a disable when we don't want it +*/ +function do_init() { + if ($('#id_code')[0].tagName == 'SELECT') { + // code is a SELECT, so we add a callback to lookup titles. + $('#id_code').change(function() { + $('#id_title')[0].disabled=true; + $.getJSON('/syrup/course/new/ajax_title', {course_code: $(this).val()}, + function(resp) { + $('#id_title').val(resp.title) + $('#id_title')[0].disabled=false; + + }); + }); + } +} + +$(do_init); diff --git a/conifer/static/jquery/js/jquery-ui-1.7.1.custom.min.js b/conifer/static/jquery/js/jquery-ui-1.7.1.custom.min.js index 529e5dd..4585414 100644 --- a/conifer/static/jquery/js/jquery-ui-1.7.1.custom.min.js +++ b/conifer/static/jquery/js/jquery-ui-1.7.1.custom.min.js @@ -1,43 +1,43 @@ -/* - * jQuery UI 1.7.1 - * - * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. - * - * http://docs.jquery.com/UI - */ jQuery.ui||(function(c){var i=c.fn.remove,d=c.browser.mozilla&&(parseFloat(c.browser.version)<1.9);c.ui={version:"1.7.1",plugin:{add:function(k,l,n){var m=c.ui[k].prototype;for(var j in n){m.plugins[j]=m.plugins[j]||[];m.plugins[j].push([l,n[j]])}},call:function(j,l,k){var n=j.plugins[l];if(!n||!j.element[0].parentNode){return}for(var m=0;m0){return true}m[j]=1;l=(m[j]>0);m[j]=0;return l},isOverAxis:function(k,j,l){return(k>j)&&(k<(j+l))},isOver:function(o,k,n,m,j,l){return c.ui.isOverAxis(o,n,j)&&c.ui.isOverAxis(k,m,l)},keyCode:{BACKSPACE:8,CAPS_LOCK:20,COMMA:188,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38}};if(d){var f=c.attr,e=c.fn.removeAttr,h="http://www.w3.org/2005/07/aaa",a=/^aria-/,b=/^wairole:/;c.attr=function(k,j,l){var m=l!==undefined;return(j=="role"?(m?f.call(this,k,j,"wairole:"+l):(f.apply(this,arguments)||"").replace(b,"")):(a.test(j)?(m?k.setAttributeNS(h,j.replace(a,"aaa:"),l):f.call(this,k,j.replace(a,"aaa:"))):f.apply(this,arguments)))};c.fn.removeAttr=function(j){return(a.test(j)?this.each(function(){this.removeAttributeNS(h,j.replace(a,""))}):e.call(this,j))}}c.fn.extend({remove:function(){c("*",this).add(this).each(function(){c(this).triggerHandler("remove")});return i.apply(this,arguments)},enableSelection:function(){return this.attr("unselectable","off").css("MozUserSelect","").unbind("selectstart.ui")},disableSelection:function(){return this.attr("unselectable","on").css("MozUserSelect","none").bind("selectstart.ui",function(){return false})},scrollParent:function(){var j;if((c.browser.msie&&(/(static|relative)/).test(this.css("position")))||(/absolute/).test(this.css("position"))){j=this.parents().filter(function(){return(/(relative|absolute|fixed)/).test(c.curCSS(this,"position",1))&&(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}else{j=this.parents().filter(function(){return(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}return(/fixed/).test(this.css("position"))||!j.length?c(document):j}});c.extend(c.expr[":"],{data:function(l,k,j){return !!c.data(l,j[3])},focusable:function(k){var l=k.nodeName.toLowerCase(),j=c.attr(k,"tabindex");return(/input|select|textarea|button|object/.test(l)?!k.disabled:"a"==l||"area"==l?k.href||!isNaN(j):!isNaN(j))&&!c(k)["area"==l?"parents":"closest"](":hidden").length},tabbable:function(k){var j=c.attr(k,"tabindex");return(isNaN(j)||j>=0)&&c(k).is(":focusable")}});function g(m,n,o,l){function k(q){var p=c[m][n][q]||[];return(typeof p=="string"?p.split(/,?\s+/):p)}var j=k("getter");if(l.length==1&&typeof l[0]=="string"){j=j.concat(k("getterSetter"))}return(c.inArray(o,j)!=-1)}c.widget=function(k,j){var l=k.split(".")[0];k=k.split(".")[1];c.fn[k]=function(p){var n=(typeof p=="string"),o=Array.prototype.slice.call(arguments,1);if(n&&p.substring(0,1)=="_"){return this}if(n&&g(l,k,p,o)){var m=c.data(this[0],k);return(m?m[p].apply(m,o):undefined)}return this.each(function(){var q=c.data(this,k);(!q&&!n&&c.data(this,k,new c[l][k](this,p))._init());(q&&n&&c.isFunction(q[p])&&q[p].apply(q,o))})};c[l]=c[l]||{};c[l][k]=function(o,n){var m=this;this.namespace=l;this.widgetName=k;this.widgetEventPrefix=c[l][k].eventPrefix||k;this.widgetBaseClass=l+"-"+k;this.options=c.extend({},c.widget.defaults,c[l][k].defaults,c.metadata&&c.metadata.get(o)[k],n);this.element=c(o).bind("setData."+k,function(q,p,r){if(q.target==o){return m._setData(p,r)}}).bind("getData."+k,function(q,p){if(q.target==o){return m._getData(p)}}).bind("remove",function(){return m.destroy()})};c[l][k].prototype=c.extend({},c.widget.prototype,j);c[l][k].getterSetter="option"};c.widget.prototype={_init:function(){},destroy:function(){this.element.removeData(this.widgetName).removeClass(this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").removeAttr("aria-disabled")},option:function(l,m){var k=l,j=this;if(typeof l=="string"){if(m===undefined){return this._getData(l)}k={};k[l]=m}c.each(k,function(n,o){j._setData(n,o)})},_getData:function(j){return this.options[j]},_setData:function(j,k){this.options[j]=k;if(j=="disabled"){this.element[k?"addClass":"removeClass"](this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").attr("aria-disabled",k)}},enable:function(){this._setData("disabled",false)},disable:function(){this._setData("disabled",true)},_trigger:function(l,m,n){var p=this.options[l],j=(l==this.widgetEventPrefix?l:this.widgetEventPrefix+l);m=c.Event(m);m.type=j;if(m.originalEvent){for(var k=c.event.props.length,o;k;){o=c.event.props[--k];m[o]=m.originalEvent[o]}}this.element.trigger(m,n);return !(c.isFunction(p)&&p.call(this.element[0],m,n)===false||m.isDefaultPrevented())}};c.widget.defaults={disabled:false};c.ui.mouse={_mouseInit:function(){var j=this;this.element.bind("mousedown."+this.widgetName,function(k){return j._mouseDown(k)}).bind("click."+this.widgetName,function(k){if(j._preventClickEvent){j._preventClickEvent=false;k.stopImmediatePropagation();return false}});if(c.browser.msie){this._mouseUnselectable=this.element.attr("unselectable");this.element.attr("unselectable","on")}this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName);(c.browser.msie&&this.element.attr("unselectable",this._mouseUnselectable))},_mouseDown:function(l){l.originalEvent=l.originalEvent||{};if(l.originalEvent.mouseHandled){return}(this._mouseStarted&&this._mouseUp(l));this._mouseDownEvent=l;var k=this,m=(l.which==1),j=(typeof this.options.cancel=="string"?c(l.target).parents().add(l.target).filter(this.options.cancel).length:false);if(!m||j||!this._mouseCapture(l)){return true}this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet){this._mouseDelayTimer=setTimeout(function(){k.mouseDelayMet=true},this.options.delay)}if(this._mouseDistanceMet(l)&&this._mouseDelayMet(l)){this._mouseStarted=(this._mouseStart(l)!==false);if(!this._mouseStarted){l.preventDefault();return true}}this._mouseMoveDelegate=function(n){return k._mouseMove(n)};this._mouseUpDelegate=function(n){return k._mouseUp(n)};c(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);(c.browser.safari||l.preventDefault());l.originalEvent.mouseHandled=true;return true},_mouseMove:function(j){if(c.browser.msie&&!j.button){return this._mouseUp(j)}if(this._mouseStarted){this._mouseDrag(j);return j.preventDefault()}if(this._mouseDistanceMet(j)&&this._mouseDelayMet(j)){this._mouseStarted=(this._mouseStart(this._mouseDownEvent,j)!==false);(this._mouseStarted?this._mouseDrag(j):this._mouseUp(j))}return !this._mouseStarted},_mouseUp:function(j){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;this._preventClickEvent=(j.target==this._mouseDownEvent.target);this._mouseStop(j)}return false},_mouseDistanceMet:function(j){return(Math.max(Math.abs(this._mouseDownEvent.pageX-j.pageX),Math.abs(this._mouseDownEvent.pageY-j.pageY))>=this.options.distance)},_mouseDelayMet:function(j){return this.mouseDelayMet},_mouseStart:function(j){},_mouseDrag:function(j){},_mouseStop:function(j){},_mouseCapture:function(j){return true}};c.ui.mouse.defaults={cancel:null,distance:1,delay:0}})(jQuery);;/* - * jQuery UI Draggable 1.7.1 - * - * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. - * - * http://docs.jquery.com/UI/Draggables - * - * Depends: - * ui.core.js - */ (function(a){a.widget("ui.draggable",a.extend({},a.ui.mouse,{_init:function(){if(this.options.helper=="original"&&!(/^(?:r|a|f)/).test(this.element.css("position"))){this.element[0].style.position="relative"}(this.options.addClasses&&this.element.addClass("ui-draggable"));(this.options.disabled&&this.element.addClass("ui-draggable-disabled"));this._mouseInit()},destroy:function(){if(!this.element.data("draggable")){return}this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy()},_mouseCapture:function(b){var c=this.options;if(this.helper||c.disabled||a(b.target).is(".ui-resizable-handle")){return false}this.handle=this._getHandle(b);if(!this.handle){return false}return true},_mouseStart:function(b){var c=this.options;this.helper=this._createHelper(b);this._cacheHelperProportions();if(a.ui.ddmanager){a.ui.ddmanager.current=this}this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(b);this.originalPageX=b.pageX;this.originalPageY=b.pageY;if(c.cursorAt){this._adjustOffsetFromHelper(c.cursorAt)}if(c.containment){this._setContainment()}this._trigger("start",b);this._cacheHelperProportions();if(a.ui.ddmanager&&!c.dropBehaviour){a.ui.ddmanager.prepareOffsets(this,b)}this.helper.addClass("ui-draggable-dragging");this._mouseDrag(b,true);return true},_mouseDrag:function(b,d){this.position=this._generatePosition(b);this.positionAbs=this._convertPositionTo("absolute");if(!d){var c=this._uiHash();this._trigger("drag",b,c);this.position=c.position}if(!this.options.axis||this.options.axis!="y"){this.helper[0].style.left=this.position.left+"px"}if(!this.options.axis||this.options.axis!="x"){this.helper[0].style.top=this.position.top+"px"}if(a.ui.ddmanager){a.ui.ddmanager.drag(this,b)}return false},_mouseStop:function(c){var d=false;if(a.ui.ddmanager&&!this.options.dropBehaviour){d=a.ui.ddmanager.drop(this,c)}if(this.dropped){d=this.dropped;this.dropped=false}if((this.options.revert=="invalid"&&!d)||(this.options.revert=="valid"&&d)||this.options.revert===true||(a.isFunction(this.options.revert)&&this.options.revert.call(this.element,d))){var b=this;a(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){b._trigger("stop",c);b._clear()})}else{this._trigger("stop",c);this._clear()}return false},_getHandle:function(b){var c=!this.options.handle||!a(this.options.handle,this.element).length?true:false;a(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==b.target){c=true}});return c},_createHelper:function(c){var d=this.options;var b=a.isFunction(d.helper)?a(d.helper.apply(this.element[0],[c])):(d.helper=="clone"?this.element.clone():this.element);if(!b.parents("body").length){b.appendTo((d.appendTo=="parent"?this.element[0].parentNode:d.appendTo))}if(b[0]!=this.element[0]&&!(/(fixed|absolute)/).test(b.css("position"))){b.css("position","absolute")}return b},_adjustOffsetFromHelper:function(b){if(b.left!=undefined){this.offset.click.left=b.left+this.margins.left}if(b.right!=undefined){this.offset.click.left=this.helperProportions.width-b.right+this.margins.left}if(b.top!=undefined){this.offset.click.top=b.top+this.margins.top}if(b.bottom!=undefined){this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top}},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])){b.left+=this.scrollParent.scrollLeft();b.top+=this.scrollParent.scrollTop()}if((this.offsetParent[0]==document.body)||(this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)){b={top:0,left:0}}return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var b=this.element.position();return{top:b.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:b.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else{return{top:0,left:0}}},_cacheMargins:function(){this.margins={left:(parseInt(this.element.css("marginLeft"),10)||0),top:(parseInt(this.element.css("marginTop"),10)||0)}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e=this.options;if(e.containment=="parent"){e.containment=this.helper[0].parentNode}if(e.containment=="document"||e.containment=="window"){this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,a(e.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a(e.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]}if(!(/^(document|window|parent)$/).test(e.containment)&&e.containment.constructor!=Array){var c=a(e.containment)[0];if(!c){return}var d=a(e.containment).offset();var b=(a(c).css("overflow")!="hidden");this.containment=[d.left+(parseInt(a(c).css("borderLeftWidth"),10)||0)+(parseInt(a(c).css("paddingLeft"),10)||0)-this.margins.left,d.top+(parseInt(a(c).css("borderTopWidth"),10)||0)+(parseInt(a(c).css("paddingTop"),10)||0)-this.margins.top,d.left+(b?Math.max(c.scrollWidth,c.offsetWidth):c.offsetWidth)-(parseInt(a(c).css("borderLeftWidth"),10)||0)-(parseInt(a(c).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,d.top+(b?Math.max(c.scrollHeight,c.offsetHeight):c.offsetHeight)-(parseInt(a(c).css("borderTopWidth"),10)||0)-(parseInt(a(c).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}else{if(e.containment.constructor==Array){this.containment=e.containment}}},_convertPositionTo:function(f,h){if(!h){h=this.position}var c=f=="absolute"?1:-1;var e=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=(/(html|body)/i).test(b[0].tagName);return{top:(h.top+this.offset.relative.top*c+this.offset.parent.top*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(g?0:b.scrollTop()))*c)),left:(h.left+this.offset.relative.left*c+this.offset.parent.left*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:b.scrollLeft())*c))}},_generatePosition:function(e){var h=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,i=(/(html|body)/i).test(b[0].tagName);if(this.cssPosition=="relative"&&!(this.scrollParent[0]!=document&&this.scrollParent[0]!=this.offsetParent[0])){this.offset.relative=this._getRelativeOffset()}var d=e.pageX;var c=e.pageY;if(this.originalPosition){if(this.containment){if(e.pageX-this.offset.click.leftthis.containment[2]){d=this.containment[2]+this.offset.click.left}if(e.pageY-this.offset.click.top>this.containment[3]){c=this.containment[3]+this.offset.click.top}}if(h.grid){var g=this.originalPageY+Math.round((c-this.originalPageY)/h.grid[1])*h.grid[1];c=this.containment?(!(g-this.offset.click.topthis.containment[3])?g:(!(g-this.offset.click.topthis.containment[2])?f:(!(f-this.offset.click.left').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1000}).css(a(this).offset()).appendTo("body")})},stop:function(b,c){a("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)})}});a.ui.plugin.add("draggable","opacity",{start:function(c,d){var b=a(d.helper),e=a(this).data("draggable").options;if(b.css("opacity")){e._opacity=b.css("opacity")}b.css("opacity",e.opacity)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._opacity){a(c.helper).css("opacity",d._opacity)}}});a.ui.plugin.add("draggable","scroll",{start:function(c,d){var b=a(this).data("draggable");if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!="HTML"){b.overflowOffset=b.scrollParent.offset()}},drag:function(d,e){var c=a(this).data("draggable"),f=c.options,b=false;if(c.scrollParent[0]!=document&&c.scrollParent[0].tagName!="HTML"){if(!f.axis||f.axis!="x"){if((c.overflowOffset.top+c.scrollParent[0].offsetHeight)-d.pageY=0;v--){var s=g.snapElements[v].left,n=s+g.snapElements[v].width,m=g.snapElements[v].top,A=m+g.snapElements[v].height;if(!((s-y=p&&n<=k)||(m>=p&&m<=k)||(nk))&&((e>=g&&e<=c)||(d>=g&&d<=c)||(ec));break;default:return false;break}};a.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,g){var b=a.ui.ddmanager.droppables[e.options.scope];var f=g?g.type:null;var h=(e.currentItem||e.element).find(":data(droppable)").andSelf();droppablesLoop:for(var d=0;d0){return true}m[j]=1;l=(m[j]>0);m[j]=0;return l},isOverAxis:function(k,j,l){return(k>j)&&(k<(j+l))},isOver:function(o,k,n,m,j,l){return c.ui.isOverAxis(o,n,j)&&c.ui.isOverAxis(k,m,l)},keyCode:{BACKSPACE:8,CAPS_LOCK:20,COMMA:188,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38}};if(d){var f=c.attr,e=c.fn.removeAttr,h="http://www.w3.org/2005/07/aaa",a=/^aria-/,b=/^wairole:/;c.attr=function(k,j,l){var m=l!==undefined;return(j=="role"?(m?f.call(this,k,j,"wairole:"+l):(f.apply(this,arguments)||"").replace(b,"")):(a.test(j)?(m?k.setAttributeNS(h,j.replace(a,"aaa:"),l):f.call(this,k,j.replace(a,"aaa:"))):f.apply(this,arguments)))};c.fn.removeAttr=function(j){return(a.test(j)?this.each(function(){this.removeAttributeNS(h,j.replace(a,""))}):e.call(this,j))}}c.fn.extend({remove:function(){c("*",this).add(this).each(function(){c(this).triggerHandler("remove")});return i.apply(this,arguments)},enableSelection:function(){return this.attr("unselectable","off").css("MozUserSelect","").unbind("selectstart.ui")},disableSelection:function(){return this.attr("unselectable","on").css("MozUserSelect","none").bind("selectstart.ui",function(){return false})},scrollParent:function(){var j;if((c.browser.msie&&(/(static|relative)/).test(this.css("position")))||(/absolute/).test(this.css("position"))){j=this.parents().filter(function(){return(/(relative|absolute|fixed)/).test(c.curCSS(this,"position",1))&&(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}else{j=this.parents().filter(function(){return(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}return(/fixed/).test(this.css("position"))||!j.length?c(document):j}});c.extend(c.expr[":"],{data:function(l,k,j){return !!c.data(l,j[3])},focusable:function(k){var l=k.nodeName.toLowerCase(),j=c.attr(k,"tabindex");return(/input|select|textarea|button|object/.test(l)?!k.disabled:"a"==l||"area"==l?k.href||!isNaN(j):!isNaN(j))&&!c(k)["area"==l?"parents":"closest"](":hidden").length},tabbable:function(k){var j=c.attr(k,"tabindex");return(isNaN(j)||j>=0)&&c(k).is(":focusable")}});function g(m,n,o,l){function k(q){var p=c[m][n][q]||[];return(typeof p=="string"?p.split(/,?\s+/):p)}var j=k("getter");if(l.length==1&&typeof l[0]=="string"){j=j.concat(k("getterSetter"))}return(c.inArray(o,j)!=-1)}c.widget=function(k,j){var l=k.split(".")[0];k=k.split(".")[1];c.fn[k]=function(p){var n=(typeof p=="string"),o=Array.prototype.slice.call(arguments,1);if(n&&p.substring(0,1)=="_"){return this}if(n&&g(l,k,p,o)){var m=c.data(this[0],k);return(m?m[p].apply(m,o):undefined)}return this.each(function(){var q=c.data(this,k);(!q&&!n&&c.data(this,k,new c[l][k](this,p))._init());(q&&n&&c.isFunction(q[p])&&q[p].apply(q,o))})};c[l]=c[l]||{};c[l][k]=function(o,n){var m=this;this.namespace=l;this.widgetName=k;this.widgetEventPrefix=c[l][k].eventPrefix||k;this.widgetBaseClass=l+"-"+k;this.options=c.extend({},c.widget.defaults,c[l][k].defaults,c.metadata&&c.metadata.get(o)[k],n);this.element=c(o).bind("setData."+k,function(q,p,r){if(q.target==o){return m._setData(p,r)}}).bind("getData."+k,function(q,p){if(q.target==o){return m._getData(p)}}).bind("remove",function(){return m.destroy()})};c[l][k].prototype=c.extend({},c.widget.prototype,j);c[l][k].getterSetter="option"};c.widget.prototype={_init:function(){},destroy:function(){this.element.removeData(this.widgetName).removeClass(this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").removeAttr("aria-disabled")},option:function(l,m){var k=l,j=this;if(typeof l=="string"){if(m===undefined){return this._getData(l)}k={};k[l]=m}c.each(k,function(n,o){j._setData(n,o)})},_getData:function(j){return this.options[j]},_setData:function(j,k){this.options[j]=k;if(j=="disabled"){this.element[k?"addClass":"removeClass"](this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").attr("aria-disabled",k)}},enable:function(){this._setData("disabled",false)},disable:function(){this._setData("disabled",true)},_trigger:function(l,m,n){var p=this.options[l],j=(l==this.widgetEventPrefix?l:this.widgetEventPrefix+l);m=c.Event(m);m.type=j;if(m.originalEvent){for(var k=c.event.props.length,o;k;){o=c.event.props[--k];m[o]=m.originalEvent[o]}}this.element.trigger(m,n);return !(c.isFunction(p)&&p.call(this.element[0],m,n)===false||m.isDefaultPrevented())}};c.widget.defaults={disabled:false};c.ui.mouse={_mouseInit:function(){var j=this;this.element.bind("mousedown."+this.widgetName,function(k){return j._mouseDown(k)}).bind("click."+this.widgetName,function(k){if(j._preventClickEvent){j._preventClickEvent=false;k.stopImmediatePropagation();return false}});if(c.browser.msie){this._mouseUnselectable=this.element.attr("unselectable");this.element.attr("unselectable","on")}this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName);(c.browser.msie&&this.element.attr("unselectable",this._mouseUnselectable))},_mouseDown:function(l){l.originalEvent=l.originalEvent||{};if(l.originalEvent.mouseHandled){return}(this._mouseStarted&&this._mouseUp(l));this._mouseDownEvent=l;var k=this,m=(l.which==1),j=(typeof this.options.cancel=="string"?c(l.target).parents().add(l.target).filter(this.options.cancel).length:false);if(!m||j||!this._mouseCapture(l)){return true}this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet){this._mouseDelayTimer=setTimeout(function(){k.mouseDelayMet=true},this.options.delay)}if(this._mouseDistanceMet(l)&&this._mouseDelayMet(l)){this._mouseStarted=(this._mouseStart(l)!==false);if(!this._mouseStarted){l.preventDefault();return true}}this._mouseMoveDelegate=function(n){return k._mouseMove(n)};this._mouseUpDelegate=function(n){return k._mouseUp(n)};c(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);(c.browser.safari||l.preventDefault());l.originalEvent.mouseHandled=true;return true},_mouseMove:function(j){if(c.browser.msie&&!j.button){return this._mouseUp(j)}if(this._mouseStarted){this._mouseDrag(j);return j.preventDefault()}if(this._mouseDistanceMet(j)&&this._mouseDelayMet(j)){this._mouseStarted=(this._mouseStart(this._mouseDownEvent,j)!==false);(this._mouseStarted?this._mouseDrag(j):this._mouseUp(j))}return !this._mouseStarted},_mouseUp:function(j){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;this._preventClickEvent=(j.target==this._mouseDownEvent.target);this._mouseStop(j)}return false},_mouseDistanceMet:function(j){return(Math.max(Math.abs(this._mouseDownEvent.pageX-j.pageX),Math.abs(this._mouseDownEvent.pageY-j.pageY))>=this.options.distance)},_mouseDelayMet:function(j){return this.mouseDelayMet},_mouseStart:function(j){},_mouseDrag:function(j){},_mouseStop:function(j){},_mouseCapture:function(j){return true}};c.ui.mouse.defaults={cancel:null,distance:1,delay:0}})(jQuery);;/* + * jQuery UI Draggable 1.7.1 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Draggables + * + * Depends: + * ui.core.js + */ (function(a){a.widget("ui.draggable",a.extend({},a.ui.mouse,{_init:function(){if(this.options.helper=="original"&&!(/^(?:r|a|f)/).test(this.element.css("position"))){this.element[0].style.position="relative"}(this.options.addClasses&&this.element.addClass("ui-draggable"));(this.options.disabled&&this.element.addClass("ui-draggable-disabled"));this._mouseInit()},destroy:function(){if(!this.element.data("draggable")){return}this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy()},_mouseCapture:function(b){var c=this.options;if(this.helper||c.disabled||a(b.target).is(".ui-resizable-handle")){return false}this.handle=this._getHandle(b);if(!this.handle){return false}return true},_mouseStart:function(b){var c=this.options;this.helper=this._createHelper(b);this._cacheHelperProportions();if(a.ui.ddmanager){a.ui.ddmanager.current=this}this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(b);this.originalPageX=b.pageX;this.originalPageY=b.pageY;if(c.cursorAt){this._adjustOffsetFromHelper(c.cursorAt)}if(c.containment){this._setContainment()}this._trigger("start",b);this._cacheHelperProportions();if(a.ui.ddmanager&&!c.dropBehaviour){a.ui.ddmanager.prepareOffsets(this,b)}this.helper.addClass("ui-draggable-dragging");this._mouseDrag(b,true);return true},_mouseDrag:function(b,d){this.position=this._generatePosition(b);this.positionAbs=this._convertPositionTo("absolute");if(!d){var c=this._uiHash();this._trigger("drag",b,c);this.position=c.position}if(!this.options.axis||this.options.axis!="y"){this.helper[0].style.left=this.position.left+"px"}if(!this.options.axis||this.options.axis!="x"){this.helper[0].style.top=this.position.top+"px"}if(a.ui.ddmanager){a.ui.ddmanager.drag(this,b)}return false},_mouseStop:function(c){var d=false;if(a.ui.ddmanager&&!this.options.dropBehaviour){d=a.ui.ddmanager.drop(this,c)}if(this.dropped){d=this.dropped;this.dropped=false}if((this.options.revert=="invalid"&&!d)||(this.options.revert=="valid"&&d)||this.options.revert===true||(a.isFunction(this.options.revert)&&this.options.revert.call(this.element,d))){var b=this;a(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){b._trigger("stop",c);b._clear()})}else{this._trigger("stop",c);this._clear()}return false},_getHandle:function(b){var c=!this.options.handle||!a(this.options.handle,this.element).length?true:false;a(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==b.target){c=true}});return c},_createHelper:function(c){var d=this.options;var b=a.isFunction(d.helper)?a(d.helper.apply(this.element[0],[c])):(d.helper=="clone"?this.element.clone():this.element);if(!b.parents("body").length){b.appendTo((d.appendTo=="parent"?this.element[0].parentNode:d.appendTo))}if(b[0]!=this.element[0]&&!(/(fixed|absolute)/).test(b.css("position"))){b.css("position","absolute")}return b},_adjustOffsetFromHelper:function(b){if(b.left!=undefined){this.offset.click.left=b.left+this.margins.left}if(b.right!=undefined){this.offset.click.left=this.helperProportions.width-b.right+this.margins.left}if(b.top!=undefined){this.offset.click.top=b.top+this.margins.top}if(b.bottom!=undefined){this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top}},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])){b.left+=this.scrollParent.scrollLeft();b.top+=this.scrollParent.scrollTop()}if((this.offsetParent[0]==document.body)||(this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)){b={top:0,left:0}}return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var b=this.element.position();return{top:b.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:b.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else{return{top:0,left:0}}},_cacheMargins:function(){this.margins={left:(parseInt(this.element.css("marginLeft"),10)||0),top:(parseInt(this.element.css("marginTop"),10)||0)}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e=this.options;if(e.containment=="parent"){e.containment=this.helper[0].parentNode}if(e.containment=="document"||e.containment=="window"){this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,a(e.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a(e.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]}if(!(/^(document|window|parent)$/).test(e.containment)&&e.containment.constructor!=Array){var c=a(e.containment)[0];if(!c){return}var d=a(e.containment).offset();var b=(a(c).css("overflow")!="hidden");this.containment=[d.left+(parseInt(a(c).css("borderLeftWidth"),10)||0)+(parseInt(a(c).css("paddingLeft"),10)||0)-this.margins.left,d.top+(parseInt(a(c).css("borderTopWidth"),10)||0)+(parseInt(a(c).css("paddingTop"),10)||0)-this.margins.top,d.left+(b?Math.max(c.scrollWidth,c.offsetWidth):c.offsetWidth)-(parseInt(a(c).css("borderLeftWidth"),10)||0)-(parseInt(a(c).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,d.top+(b?Math.max(c.scrollHeight,c.offsetHeight):c.offsetHeight)-(parseInt(a(c).css("borderTopWidth"),10)||0)-(parseInt(a(c).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}else{if(e.containment.constructor==Array){this.containment=e.containment}}},_convertPositionTo:function(f,h){if(!h){h=this.position}var c=f=="absolute"?1:-1;var e=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=(/(html|body)/i).test(b[0].tagName);return{top:(h.top+this.offset.relative.top*c+this.offset.parent.top*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(g?0:b.scrollTop()))*c)),left:(h.left+this.offset.relative.left*c+this.offset.parent.left*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:b.scrollLeft())*c))}},_generatePosition:function(e){var h=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,i=(/(html|body)/i).test(b[0].tagName);if(this.cssPosition=="relative"&&!(this.scrollParent[0]!=document&&this.scrollParent[0]!=this.offsetParent[0])){this.offset.relative=this._getRelativeOffset()}var d=e.pageX;var c=e.pageY;if(this.originalPosition){if(this.containment){if(e.pageX-this.offset.click.leftthis.containment[2]){d=this.containment[2]+this.offset.click.left}if(e.pageY-this.offset.click.top>this.containment[3]){c=this.containment[3]+this.offset.click.top}}if(h.grid){var g=this.originalPageY+Math.round((c-this.originalPageY)/h.grid[1])*h.grid[1];c=this.containment?(!(g-this.offset.click.topthis.containment[3])?g:(!(g-this.offset.click.topthis.containment[2])?f:(!(f-this.offset.click.left').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1000}).css(a(this).offset()).appendTo("body")})},stop:function(b,c){a("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)})}});a.ui.plugin.add("draggable","opacity",{start:function(c,d){var b=a(d.helper),e=a(this).data("draggable").options;if(b.css("opacity")){e._opacity=b.css("opacity")}b.css("opacity",e.opacity)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._opacity){a(c.helper).css("opacity",d._opacity)}}});a.ui.plugin.add("draggable","scroll",{start:function(c,d){var b=a(this).data("draggable");if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!="HTML"){b.overflowOffset=b.scrollParent.offset()}},drag:function(d,e){var c=a(this).data("draggable"),f=c.options,b=false;if(c.scrollParent[0]!=document&&c.scrollParent[0].tagName!="HTML"){if(!f.axis||f.axis!="x"){if((c.overflowOffset.top+c.scrollParent[0].offsetHeight)-d.pageY=0;v--){var s=g.snapElements[v].left,n=s+g.snapElements[v].width,m=g.snapElements[v].top,A=m+g.snapElements[v].height;if(!((s-y=p&&n<=k)||(m>=p&&m<=k)||(nk))&&((e>=g&&e<=c)||(d>=g&&d<=c)||(ec));break;default:return false;break}};a.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,g){var b=a.ui.ddmanager.droppables[e.options.scope];var f=g?g.type:null;var h=(e.currentItem||e.element).find(":data(droppable)").andSelf();droppablesLoop:for(var d=0;d=0;b--){this.items[b].item.removeData("sortable-item")}},_mouseCapture:function(e,f){if(this.reverting){return false}if(this.options.disabled||this.options.type=="static"){return false}this._refreshItems(e);var d=null,c=this,b=a(e.target).parents().each(function(){if(a.data(this,"sortable-item")==c){d=a(this);return false}});if(a.data(e.target,"sortable-item")==c){d=a(e.target)}if(!d){return false}if(this.options.handle&&!f){var g=false;a(this.options.handle,d).find("*").andSelf().each(function(){if(this==e.target){g=true}});if(!g){return false}}this.currentItem=d;this._removeCurrentsFromItems();return true},_mouseStart:function(e,f,b){var g=this.options,c=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(e);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");a.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(e);this.originalPageX=e.pageX;this.originalPageY=e.pageY;if(g.cursorAt){this._adjustOffsetFromHelper(g.cursorAt)}this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]};if(this.helper[0]!=this.currentItem[0]){this.currentItem.hide()}this._createPlaceholder();if(g.containment){this._setContainment()}if(g.cursor){if(a("body").css("cursor")){this._storedCursor=a("body").css("cursor")}a("body").css("cursor",g.cursor)}if(g.opacity){if(this.helper.css("opacity")){this._storedOpacity=this.helper.css("opacity")}this.helper.css("opacity",g.opacity)}if(g.zIndex){if(this.helper.css("zIndex")){this._storedZIndex=this.helper.css("zIndex")}this.helper.css("zIndex",g.zIndex)}if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){this.overflowOffset=this.scrollParent.offset()}this._trigger("start",e,this._uiHash());if(!this._preserveHelperProportions){this._cacheHelperProportions()}if(!b){for(var d=this.containers.length-1;d>=0;d--){this.containers[d]._trigger("activate",e,c._uiHash(this))}}if(a.ui.ddmanager){a.ui.ddmanager.current=this}if(a.ui.ddmanager&&!g.dropBehaviour){a.ui.ddmanager.prepareOffsets(this,e)}this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(e);return true},_mouseDrag:function(f){this.position=this._generatePosition(f);this.positionAbs=this._convertPositionTo("absolute");if(!this.lastPositionAbs){this.lastPositionAbs=this.positionAbs}if(this.options.scroll){var g=this.options,b=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if((this.overflowOffset.top+this.scrollParent[0].offsetHeight)-f.pageY=0;d--){var e=this.items[d],c=e.item[0],h=this._intersectsWithPointer(e);if(!h){continue}if(c!=this.currentItem[0]&&this.placeholder[h==1?"next":"prev"]()[0]!=c&&!a.ui.contains(this.placeholder[0],c)&&(this.options.type=="semi-dynamic"?!a.ui.contains(this.element[0],c):true)){this.direction=h==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(e)){this._rearrange(f,e)}else{break}this._trigger("change",f,this._uiHash());break}}this._contactContainers(f);if(a.ui.ddmanager){a.ui.ddmanager.drag(this,f)}this._trigger("sort",f,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(c,d){if(!c){return}if(a.ui.ddmanager&&!this.options.dropBehaviour){a.ui.ddmanager.drop(this,c)}if(this.options.revert){var b=this;var e=b.placeholder.offset();b.reverting=true;a(this.helper).animate({left:e.left-this.offset.parent.left-b.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:e.top-this.offset.parent.top-b.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){b._clear(c)})}else{this._clear(c,d)}return false},cancel:function(){var b=this;if(this.dragging){this._mouseUp();if(this.options.helper=="original"){this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else{this.currentItem.show()}for(var c=this.containers.length-1;c>=0;c--){this.containers[c]._trigger("deactivate",null,b._uiHash(this));if(this.containers[c].containerCache.over){this.containers[c]._trigger("out",null,b._uiHash(this));this.containers[c].containerCache.over=0}}}if(this.placeholder[0].parentNode){this.placeholder[0].parentNode.removeChild(this.placeholder[0])}if(this.options.helper!="original"&&this.helper&&this.helper[0].parentNode){this.helper.remove()}a.extend(this,{helper:null,dragging:false,reverting:false,_noFinalSort:null});if(this.domPosition.prev){a(this.domPosition.prev).after(this.currentItem)}else{a(this.domPosition.parent).prepend(this.currentItem)}return true},serialize:function(d){var b=this._getItemsAsjQuery(d&&d.connected);var c=[];d=d||{};a(b).each(function(){var e=(a(d.item||this).attr(d.attribute||"id")||"").match(d.expression||(/(.+)[-=_](.+)/));if(e){c.push((d.key||e[1]+"[]")+"="+(d.key&&d.expression?e[1]:e[2]))}});return c.join("&")},toArray:function(d){var b=this._getItemsAsjQuery(d&&d.connected);var c=[];d=d||{};b.each(function(){c.push(a(d.item||this).attr(d.attribute||"id")||"")});return c},_intersectsWith:function(m){var e=this.positionAbs.left,d=e+this.helperProportions.width,k=this.positionAbs.top,j=k+this.helperProportions.height;var f=m.left,c=f+m.width,n=m.top,i=n+m.height;var o=this.offset.click.top,h=this.offset.click.left;var g=(k+o)>n&&(k+o)f&&(e+h)m[this.floating?"width":"height"])){return g}else{return(f0?"down":"up")},_getDragHorizontalDirection:function(){var b=this.positionAbs.left-this.lastPositionAbs.left;return b!=0&&(b>0?"right":"left")},refresh:function(b){this._refreshItems(b);this.refreshPositions()},_connectWith:function(){var b=this.options;return b.connectWith.constructor==String?[b.connectWith]:b.connectWith},_getItemsAsjQuery:function(b){var l=this;var g=[];var e=[];var h=this._connectWith();if(h&&b){for(var d=h.length-1;d>=0;d--){var k=a(h[d]);for(var c=k.length-1;c>=0;c--){var f=a.data(k[c],"sortable");if(f&&f!=this&&!f.options.disabled){e.push([a.isFunction(f.options.items)?f.options.items.call(f.element):a(f.options.items,f.element).not(".ui-sortable-helper"),f])}}}}e.push([a.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):a(this.options.items,this.element).not(".ui-sortable-helper"),this]);for(var d=e.length-1;d>=0;d--){e[d][0].each(function(){g.push(this)})}return a(g)},_removeCurrentsFromItems:function(){var d=this.currentItem.find(":data(sortable-item)");for(var c=0;c=0;e--){var m=a(l[e]);for(var d=m.length-1;d>=0;d--){var g=a.data(m[d],"sortable");if(g&&g!=this&&!g.options.disabled){f.push([a.isFunction(g.options.items)?g.options.items.call(g.element[0],b,{item:this.currentItem}):a(g.options.items,g.element),g]);this.containers.push(g)}}}}for(var e=f.length-1;e>=0;e--){var k=f[e][1];var c=f[e][0];for(var d=0,n=c.length;d=0;d--){var e=this.items[d];if(e.instance!=this.currentContainer&&this.currentContainer&&e.item[0]!=this.currentItem[0]){continue}var c=this.options.toleranceElement?a(this.options.toleranceElement,e.item):e.item;if(!b){e.width=c.outerWidth();e.height=c.outerHeight()}var f=c.offset();e.left=f.left;e.top=f.top}if(this.options.custom&&this.options.custom.refreshContainers){this.options.custom.refreshContainers.call(this)}else{for(var d=this.containers.length-1;d>=0;d--){var f=this.containers[d].element.offset();this.containers[d].containerCache.left=f.left;this.containers[d].containerCache.top=f.top;this.containers[d].containerCache.width=this.containers[d].element.outerWidth();this.containers[d].containerCache.height=this.containers[d].element.outerHeight()}}},_createPlaceholder:function(d){var b=d||this,e=b.options;if(!e.placeholder||e.placeholder.constructor==String){var c=e.placeholder;e.placeholder={element:function(){var f=a(document.createElement(b.currentItem[0].nodeName)).addClass(c||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!c){f.style.visibility="hidden"}return f},update:function(f,g){if(c&&!e.forcePlaceholderSize){return}if(!g.height()){g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10))}if(!g.width()){g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")||0,10))}}}}b.placeholder=a(e.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);e.placeholder.update(b,b.placeholder)},_contactContainers:function(d){for(var c=this.containers.length-1;c>=0;c--){if(this._intersectsWith(this.containers[c].containerCache)){if(!this.containers[c].containerCache.over){if(this.currentContainer!=this.containers[c]){var h=10000;var g=null;var e=this.positionAbs[this.containers[c].floating?"left":"top"];for(var b=this.items.length-1;b>=0;b--){if(!a.ui.contains(this.containers[c].element[0],this.items[b].item[0])){continue}var f=this.items[b][this.containers[c].floating?"left":"top"];if(Math.abs(f-e)this.containment[2]){d=this.containment[2]+this.offset.click.left}if(e.pageY-this.offset.click.top>this.containment[3]){c=this.containment[3]+this.offset.click.top}}if(h.grid){var g=this.originalPageY+Math.round((c-this.originalPageY)/h.grid[1])*h.grid[1];c=this.containment?(!(g-this.offset.click.topthis.containment[3])?g:(!(g-this.offset.click.topthis.containment[2])?f:(!(f-this.offset.click.left=0;c--){if(a.ui.contains(this.containers[c].element[0],this.currentItem[0])&&!e){f.push((function(g){return function(h){g._trigger("receive",h,this._uiHash(this))}}).call(this,this.containers[c]));f.push((function(g){return function(h){g._trigger("update",h,this._uiHash(this))}}).call(this,this.containers[c]))}}}for(var c=this.containers.length-1;c>=0;c--){if(!e){f.push((function(g){return function(h){g._trigger("deactivate",h,this._uiHash(this))}}).call(this,this.containers[c]))}if(this.containers[c].containerCache.over){f.push((function(g){return function(h){g._trigger("out",h,this._uiHash(this))}}).call(this,this.containers[c]));this.containers[c].containerCache.over=0}}if(this._storedCursor){a("body").css("cursor",this._storedCursor)}if(this._storedOpacity){this.helper.css("opacity",this._storedOpacity)}if(this._storedZIndex){this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex)}this.dragging=false;if(this.cancelHelperRemoval){if(!e){this._trigger("beforeStop",d,this._uiHash());for(var c=0;c *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1000}})})(jQuery);; \ No newline at end of file diff --git a/conifer/static/jquery/js/jquery.js b/conifer/static/jquery/js/jquery.js index 49e7795..97a5c83 100644 --- a/conifer/static/jquery/js/jquery.js +++ b/conifer/static/jquery/js/jquery.js @@ -536,156 +536,156 @@ function evalScript( i, elem ) { function now() { return (new Date).getTime(); } -var expando = "jQuery" + now(), uuid = 0, windowData = {}; - -jQuery.extend({ - cache: {}, - - data: function( elem, name, data ) { - elem = elem == window ? - windowData : - elem; - - var id = elem[ expando ], cache = jQuery.cache; - - // Compute a unique ID for the element - if(!id) id = elem[ expando ] = ++uuid; - - // Only generate the data cache if we're - // trying to access or manipulate it - if ( name && !cache[ id ] ) - cache[ id ] = {}; - - var thisCache = cache[ id ]; - - // Prevent overriding the named cache with undefined values - if ( data !== undefined ) thisCache[ name ] = data; - - if(name === true) return thisCache - else if(name) return thisCache[name] - else return id - }, - - removeData: function( elem, name ) { - elem = elem == window ? - windowData : - elem; - - var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ]; - - // If we want to remove a specific section of the element's data - if ( name ) { - if ( thisCache ) { - // Remove the section of cache data - delete thisCache[ name ]; - - // If we've removed all the data, remove the element's cache - if( jQuery.isEmptyObject(thisCache) ) - jQuery.removeData( elem ); - } - - // Otherwise, we want to remove all of the element's data - } else { - // Clean up the element expando - try { - delete elem[ expando ]; - } catch(e){ - // IE has trouble directly removing the expando - // but it's ok with using removeAttribute - if ( elem.removeAttribute ) - elem.removeAttribute( expando ); - } - - // Completely remove the data cache - delete cache[ id ]; - } - }, - queue: function( elem, type, data ) { - if( !elem ) return; - - type = (type || "fx") + "queue"; - var q = jQuery.data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if( !data ) return q || []; - - if ( !q || jQuery.isArray(data) ) - q = jQuery.data( elem, type, jQuery.makeArray(data) ); - else - q.push( data ); - - return q; - }, - - dequeue: function( elem, type ){ - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), fn = queue.shift(); - - // If the fx queue is dequeued, always remove the progress sentinel - if( fn === "inprogress" ) fn = queue.shift(); - - if( fn ) { - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if( type == "fx" ) queue.unshift("inprogress"); - - fn.call(elem, function() { jQuery.dequeue(elem, type); }); - } - } -}); - -jQuery.fn.extend({ - data: function( key, value ){ - if(typeof key === "undefined" && this.length) return jQuery.data(this[0], true); - - var parts = key.split("."); - parts[1] = parts[1] ? "." + parts[1] : ""; - - if ( value === undefined ) { - var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); - - if ( data === undefined && this.length ) - data = jQuery.data( this[0], key ); - - return data === undefined && parts[1] ? - this.data( parts[0] ) : - data; - } else - return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){ - jQuery.data( this, key, value ); - }); - }, - - removeData: function( key ){ - return this.each(function(){ - jQuery.removeData( this, key ); - }); - }, - queue: function(type, data){ - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - } - - if ( data === undefined ) - return jQuery.queue( this[0], type ); - - return this.each(function(i, elem){ - var queue = jQuery.queue( this, type, data ); - - if( type == "fx" && queue[0] !== "inprogress" ) - jQuery.dequeue( this, type ) - }); - }, - dequeue: function(type){ - return this.each(function(){ - jQuery.dequeue( this, type ); - }); - }, - clearQueue: function(type){ - return this.queue( type || "fx", [] ); - } +var expando = "jQuery" + now(), uuid = 0, windowData = {}; + +jQuery.extend({ + cache: {}, + + data: function( elem, name, data ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ], cache = jQuery.cache; + + // Compute a unique ID for the element + if(!id) id = elem[ expando ] = ++uuid; + + // Only generate the data cache if we're + // trying to access or manipulate it + if ( name && !cache[ id ] ) + cache[ id ] = {}; + + var thisCache = cache[ id ]; + + // Prevent overriding the named cache with undefined values + if ( data !== undefined ) thisCache[ name ] = data; + + if(name === true) return thisCache + else if(name) return thisCache[name] + else return id + }, + + removeData: function( elem, name ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ]; + + // If we want to remove a specific section of the element's data + if ( name ) { + if ( thisCache ) { + // Remove the section of cache data + delete thisCache[ name ]; + + // If we've removed all the data, remove the element's cache + if( jQuery.isEmptyObject(thisCache) ) + jQuery.removeData( elem ); + } + + // Otherwise, we want to remove all of the element's data + } else { + // Clean up the element expando + try { + delete elem[ expando ]; + } catch(e){ + // IE has trouble directly removing the expando + // but it's ok with using removeAttribute + if ( elem.removeAttribute ) + elem.removeAttribute( expando ); + } + + // Completely remove the data cache + delete cache[ id ]; + } + }, + queue: function( elem, type, data ) { + if( !elem ) return; + + type = (type || "fx") + "queue"; + var q = jQuery.data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if( !data ) return q || []; + + if ( !q || jQuery.isArray(data) ) + q = jQuery.data( elem, type, jQuery.makeArray(data) ); + else + q.push( data ); + + return q; + }, + + dequeue: function( elem, type ){ + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), fn = queue.shift(); + + // If the fx queue is dequeued, always remove the progress sentinel + if( fn === "inprogress" ) fn = queue.shift(); + + if( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if( type == "fx" ) queue.unshift("inprogress"); + + fn.call(elem, function() { jQuery.dequeue(elem, type); }); + } + } +}); + +jQuery.fn.extend({ + data: function( key, value ){ + if(typeof key === "undefined" && this.length) return jQuery.data(this[0], true); + + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + if ( data === undefined && this.length ) + data = jQuery.data( this[0], key ); + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } else + return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){ + jQuery.data( this, key, value ); + }); + }, + + removeData: function( key ){ + return this.each(function(){ + jQuery.removeData( this, key ); + }); + }, + queue: function(type, data){ + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + } + + if ( data === undefined ) + return jQuery.queue( this[0], type ); + + return this.each(function(i, elem){ + var queue = jQuery.queue( this, type, data ); + + if( type == "fx" && queue[0] !== "inprogress" ) + jQuery.dequeue( this, type ) + }); + }, + dequeue: function(type){ + return this.each(function(){ + jQuery.dequeue( this, type ); + }); + }, + clearQueue: function(type){ + return this.queue( type || "fx", [] ); + } });/*! * Sizzle CSS Selector Engine - v1.0 * Copyright 2009, The Dojo Foundation diff --git a/conifer/static/main.css b/conifer/static/main.css index 6306da2..f9c1af7 100644 --- a/conifer/static/main.css +++ b/conifer/static/main.css @@ -1,342 +1,342 @@ -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, font, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-weight: inherit; - font-style: inherit; - font-size: 100%; - font-family: inherit; - vertical-align: baseline; -} -/* remember to define focus styles! */ -:focus { - outline: 0; -} -body { - line-height: 1; - color: black; - background: white; -} -ol, ul { - list-style: none; -} -/* tables still need 'cellspacing="0"' in the markup */ -table { - border-collapse: separate; - border-spacing: 0; -} -caption, th, td { - text-align: left; - font-weight: normal; -} -blockquote:before, blockquote:after, -q:before, q:after { - content: ""; -} -blockquote, q { - quotes: "" ""; -} - -/* General look and feel */ - -body * { font-family: Verdana, sans-serif; } -pre, code { font-family: monospace; } - -body, html { margin: 0; padding: 0; } - -body { - background-color: #005; - font-size: normal; -} - -div#outer { - background-color: white; - width: 960px; margin-bottom: 50px; - margin-left: auto; margin-right: auto; -} -#mainpanel { background-color: white; padding: 0 12px 24px 12px; -min-height: 300px; -} - -/* General headers and footers */ - -#header, #footer { - color: white; padding: 8px 4px 12px 8px; - background-color: #448; -} - -#header div#search { - float: right; - margin: 0; - color: #ccc; -} - -#header #welcome { - margin-top: 4px; -} - -#brandheader { background-color: white; padding: 8px; } - -#header a { color: #fff; font-weight: bold; padding: 10px 12px 10px 12px; } -#header a.loginbutton { background-color: #a44; } -#header a:hover { background-color: #fb7; color: black; text-decoration: none; } - -tbody td, tbody th { vertical-align: top; } - -#footer { - margin: 12px; - padding-bottom: 12px; - background-color: #ddf; - color: black; - border-bottom: white 10px solid; -} - -/* heading sizes and colours. */ - -h1 { font-size: 150%; font-weight: bold; } -h2 { font-size: 125%; font-weight: bold; } -h3 { font-size: 120%; font-weight: bold; } -p { margin: 24px 0px; } -h1 { color: navy; margin: 36px 0 18px 0; } -h2 { color: #336; margin: 24px 0 12px 0; } -h3, h4 { color: darkgreen; } -h1 a, h2 a { color: navy; } - -a { color: blue; text-decoration: none; } -a:hover { text-decoration: underline; } - -/* error panel on the person-edit form */ - -.errors { margin: 1em; padding: 1em; background-color: #fdd; } -.errors h2 { font-size: 120%; } - -/* actions (e.g. "edit user" link) */ -.action a { font-weight: bold; } - -#tabbar { margin: 18px 0; padding: 0; clear: both; } -#tabbar li { display: inline; } -#tabbar li a { padding: 15px 18px 5px 18px; background-color: #ddf; color: black; text-decoration: none; } -#tabbar li a:hover { background-color: #fc8; } - -/* -#tabbar li.active a { background-color: #fa6; font-weight: bold; } -*/ - -.pagination_controls { - text-align: center; margin: 12px 0; -} - -.pagination_controls .nums { - padding: 0 12px; -} - -.pagetable td { border: #ddd 1px solid; padding: 8px; } -.pagetable .odd { - background-color: #F8F8F8; -} -.pagetable thead th { font-size: smaller; text-align: left; padding: 2px 8px; } - - -/* nested titles: like breadcrumbs when drilling into an itemtree */ - -.nestedtitle h2 { margin-top: 8px; } -.nestedtitle a { color: navy; } - -span.final_item { font-weight: bold; font-size: 110%; } - -/* item trees (tree of headings and items in a course */ - -#sidepanel { width: 183px; float: right; text-align: right;} -#sidepanel div { margin: 6px 0; } - -#treepanel { width: 740px; } - -.helptext { - margin-top: 30px; font-size: 90%; - padding: 10px; - background-color: #eef; - clear: both; -} - -.itemtree { - margin-left: 20px; - padding-left: 20px; - list-style-type: none; -} - -.itemtree .itemtree { margin-left: 30px; padding-left: 0; } - -.itemtree li { padding-left: 0; margin-left: 0; } - -.itemtree li { margin: 12px 8px; } -.itemtree li .mainline { padding-left: 8px; } - -.itemtree .metalink { padding-left: 8px; color: gray; } -.itemtree .metalink a { - color: gray; -} - -.itemtree .editlinks { padding-left: 12px; color: gray; - font-size: small; - } -.itemtree .editlinks a { color: navy; } - -.itemadd { - margin-top: 30px; font-size: 90%; - padding: 10px; - background-color: #eef; - clear: both; -} -.itemadd li { - margin: 10px; -} - -.itemadd a { color: navy; } - -/* specialized display of items in tree, by type */ - -.itemtree li.item_HEADING { - list-style-image: url(tango/folder.png); -} -.itemtree li.item_HEADING > a { - color: navy; -} - -li.item_HEADING .headingmainline { - margin-bottom: 12px; -} - -li.item_HEADING .headingmainline a.mainlink { - border-bottom: #aaa 1px solid; -} - -li.item_HEADING .headingmainline a.mainlink:hover { - border-bottom: none; -} - -.itemtree li.item_ELEC { - list-style-image: url(tango/document.png); -} - -.itemtree li.item_URL { - list-style-image: url(tango/applications-internet.png); -} - -.itemtree li.item_PHYS { - /* fixme: need a better icon */ - list-style-image: url(tango/x-office-address-book.png); -} - - -.instructors { - border: 1px solid #ccc; - float: left; - width: 50%; - padding: 2px 2px 2px 2px; - font-size: 1em; - line-height: 1em; - text-align: left; - margin-right: 5px; -} - -.topbox { - border: 1px solid #ccc; - width: 50%; - line-height: 1em; - text-align: left; -} - -table.topheading { width: 100%; } -.topheading th { - background-color: #ddf; -} - -.topheading th, .topheading td { -padding: 8px; -} - -p.todo, div.todo { background-color: #fdd; padding: 6px; margin: 12px; border-left: #d99 6px solid; } - -.newsitem p { margin: 12px 0; } -.newsitem ul { list-style: circle; margin-left: 30px; } -.newsitem ul li { margin: 8px; } - -.newsitem { - max-width: 600px; - line-height: 125%; -} -.newsitem .newsdate { - margin: 4px 0 8px 0; text-align: right; - font-size: 80%; color: navy; -} - -.menublockopener { margin-left: 0.25em; color: #bbb !important; font-weight: normal !important; } -.menublock { background-color: #f2e4cc; font-size: 95%; padding: 1px 4px; } - -#coursebanner { background-color: #f2e4cc; margin: -12px -12px 12px -12px; padding: 8px; } -#coursesearch { float: right; } -#coursebanner h1 { margin: 12px 0; font-size: 125%; } - -#edit_course_link { margin: 8px 0 8px 0; font-size: 95%; } - - -#breadcrumbs { margin: 8px 0px 16px 0; width: 716px; - text-indent: -24px; padding-left: 24px; } - -.errorlist { float: right; } -.errorlist li { color: red; font-size: 90%; } - - -/* a nice table-style for forms. */ -.formtable tbody th { - padding: 0 8px 16px 0; - width: 200px; - text-align: left; - font-size: 90%; - font-weight: normal; -} - -thead th { padding: 8px; font-weight: bold; font-size: 90%; } -.metadata_table tbody th, -.metadata_table tbody td { - padding: 8px; border: #ddd 1px solid; -} -.metadata_table input { width: 600px; } -.metadata_table .meta3 input { width: 10px; } - -.metadata_table tbody th { - background-color: #eee; -} - -.metadata_table a.bigdownload { padding: 8px 58px; font-weight: bold; font-size: 105%; } -.metadata_table a.bigdownload:hover { background-color: #dfd; color: black; } - -h2.metadata_subhead {font-size: 105%; padding: 0; margin: 18px 0 9px 0;} - -.metadata_table tbody th { - text-align: left; width: 120px; -} -.gap { height: 24px; } -.metadata_table td { max-width: 800px; overflow: hidden; } - -/* panels that appear when specific OPTIONs or radio-buttons are selected. */ -.specific { padding: 8px; margin: 0 16px; background-color: #eef; } - - -li.sort_item { margin-top: 20px !important; - border: gray 1px dotted; width: 400px; } - -li.sort_item:hover { background-color: #eee; } - -ul.heading_tree li { list-style: none; } -ul.heading_tree { margin: 0; padding-left: 0; } -ul.heading_tree ul { margin: 0; padding-left: 25px; } - +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; +} +/* remember to define focus styles! */ +:focus { + outline: 0; +} +body { + line-height: 1; + color: black; + background: white; +} +ol, ul { + list-style: none; +} +/* tables still need 'cellspacing="0"' in the markup */ +table { + border-collapse: separate; + border-spacing: 0; +} +caption, th, td { + text-align: left; + font-weight: normal; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ""; +} +blockquote, q { + quotes: "" ""; +} + +/* General look and feel */ + +body * { font-family: Verdana, sans-serif; } +pre, code { font-family: monospace; } + +body, html { margin: 0; padding: 0; } + +body { + background-color: #005; + font-size: normal; +} + +div#outer { + background-color: white; + width: 960px; margin-bottom: 50px; + margin-left: auto; margin-right: auto; +} +#mainpanel { background-color: white; padding: 0 12px 24px 12px; +min-height: 300px; +} + +/* General headers and footers */ + +#header, #footer { + color: white; padding: 8px 4px 12px 8px; + background-color: #448; +} + +#header div#search { + float: right; + margin: 0; + color: #ccc; +} + +#header #welcome { + margin-top: 4px; +} + +#brandheader { background-color: white; padding: 8px; } + +#header a { color: #fff; font-weight: bold; padding: 10px 12px 10px 12px; } +#header a.loginbutton { background-color: #a44; } +#header a:hover { background-color: #fb7; color: black; text-decoration: none; } + +tbody td, tbody th { vertical-align: top; } + +#footer { + margin: 12px; + padding-bottom: 12px; + background-color: #ddf; + color: black; + border-bottom: white 10px solid; +} + +/* heading sizes and colours. */ + +h1 { font-size: 150%; font-weight: bold; } +h2 { font-size: 125%; font-weight: bold; } +h3 { font-size: 120%; font-weight: bold; } +p { margin: 24px 0px; } +h1 { color: navy; margin: 36px 0 18px 0; } +h2 { color: #336; margin: 24px 0 12px 0; } +h3, h4 { color: darkgreen; } +h1 a, h2 a { color: navy; } + +a { color: blue; text-decoration: none; } +a:hover { text-decoration: underline; } + +/* error panel on the person-edit form */ + +.errors { margin: 1em; padding: 1em; background-color: #fdd; } +.errors h2 { font-size: 120%; } + +/* actions (e.g. "edit user" link) */ +.action a { font-weight: bold; } + +#tabbar { margin: 18px 0; padding: 0; clear: both; } +#tabbar li { display: inline; } +#tabbar li a { padding: 15px 18px 5px 18px; background-color: #ddf; color: black; text-decoration: none; } +#tabbar li a:hover { background-color: #fc8; } + +/* +#tabbar li.active a { background-color: #fa6; font-weight: bold; } +*/ + +.pagination_controls { + text-align: center; margin: 12px 0; +} + +.pagination_controls .nums { + padding: 0 12px; +} + +.pagetable td { border: #ddd 1px solid; padding: 8px; } +.pagetable .odd { + background-color: #F8F8F8; +} +.pagetable thead th { font-size: smaller; text-align: left; padding: 2px 8px; } + + +/* nested titles: like breadcrumbs when drilling into an itemtree */ + +.nestedtitle h2 { margin-top: 8px; } +.nestedtitle a { color: navy; } + +span.final_item { font-weight: bold; font-size: 110%; } + +/* item trees (tree of headings and items in a course */ + +#sidepanel { width: 183px; float: right; text-align: right;} +#sidepanel div { margin: 6px 0; } + +#treepanel { width: 740px; } + +.helptext { + margin-top: 30px; font-size: 90%; + padding: 10px; + background-color: #eef; + clear: both; +} + +.itemtree { + margin-left: 20px; + padding-left: 20px; + list-style-type: none; +} + +.itemtree .itemtree { margin-left: 30px; padding-left: 0; } + +.itemtree li { padding-left: 0; margin-left: 0; } + +.itemtree li { margin: 12px 8px; } +.itemtree li .mainline { padding-left: 8px; } + +.itemtree .metalink { padding-left: 8px; color: gray; } +.itemtree .metalink a { + color: gray; +} + +.itemtree .editlinks { padding-left: 12px; color: gray; + font-size: small; + } +.itemtree .editlinks a { color: navy; } + +.itemadd { + margin-top: 30px; font-size: 90%; + padding: 10px; + background-color: #eef; + clear: both; +} +.itemadd li { + margin: 10px; +} + +.itemadd a { color: navy; } + +/* specialized display of items in tree, by type */ + +.itemtree li.item_HEADING { + list-style-image: url(tango/folder.png); +} +.itemtree li.item_HEADING > a { + color: navy; +} + +li.item_HEADING .headingmainline { + margin-bottom: 12px; +} + +li.item_HEADING .headingmainline a.mainlink { + border-bottom: #aaa 1px solid; +} + +li.item_HEADING .headingmainline a.mainlink:hover { + border-bottom: none; +} + +.itemtree li.item_ELEC { + list-style-image: url(tango/document.png); +} + +.itemtree li.item_URL { + list-style-image: url(tango/applications-internet.png); +} + +.itemtree li.item_PHYS { + /* fixme: need a better icon */ + list-style-image: url(tango/x-office-address-book.png); +} + + +.instructors { + border: 1px solid #ccc; + float: left; + width: 50%; + padding: 2px 2px 2px 2px; + font-size: 1em; + line-height: 1em; + text-align: left; + margin-right: 5px; +} + +.topbox { + border: 1px solid #ccc; + width: 50%; + line-height: 1em; + text-align: left; +} + +table.topheading { width: 100%; } +.topheading th { + background-color: #ddf; +} + +.topheading th, .topheading td { +padding: 8px; +} + +p.todo, div.todo { background-color: #fdd; padding: 6px; margin: 12px; border-left: #d99 6px solid; } + +.newsitem p { margin: 12px 0; } +.newsitem ul { list-style: circle; margin-left: 30px; } +.newsitem ul li { margin: 8px; } + +.newsitem { + max-width: 600px; + line-height: 125%; +} +.newsitem .newsdate { + margin: 4px 0 8px 0; text-align: right; + font-size: 80%; color: navy; +} + +.menublockopener { margin-left: 0.25em; color: #bbb !important; font-weight: normal !important; } +.menublock { background-color: #f2e4cc; font-size: 95%; padding: 1px 4px; } + +#coursebanner { background-color: #f2e4cc; margin: -12px -12px 12px -12px; padding: 8px; } +#coursesearch { float: right; } +#coursebanner h1 { margin: 12px 0; font-size: 125%; } + +#edit_course_link { margin: 8px 0 8px 0; font-size: 95%; } + + +#breadcrumbs { margin: 8px 0px 16px 0; width: 716px; + text-indent: -24px; padding-left: 24px; } + +.errorlist { float: right; } +.errorlist li { color: red; font-size: 90%; } + + +/* a nice table-style for forms. */ +.formtable tbody th { + padding: 0 8px 16px 0; + width: 200px; + text-align: left; + font-size: 90%; + font-weight: normal; +} + +thead th { padding: 8px; font-weight: bold; font-size: 90%; } +.metadata_table tbody th, +.metadata_table tbody td { + padding: 8px; border: #ddd 1px solid; +} +.metadata_table input { width: 600px; } +.metadata_table .meta3 input { width: 10px; } + +.metadata_table tbody th { + background-color: #eee; +} + +.metadata_table a.bigdownload { padding: 8px 58px; font-weight: bold; font-size: 105%; } +.metadata_table a.bigdownload:hover { background-color: #dfd; color: black; } + +h2.metadata_subhead {font-size: 105%; padding: 0; margin: 18px 0 9px 0;} + +.metadata_table tbody th { + text-align: left; width: 120px; +} +.gap { height: 24px; } +.metadata_table td { max-width: 800px; overflow: hidden; } + +/* panels that appear when specific OPTIONs or radio-buttons are selected. */ +.specific { padding: 8px; margin: 0 16px; background-color: #eef; } + + +li.sort_item { margin-top: 20px !important; + border: gray 1px dotted; width: 400px; } + +li.sort_item:hover { background-color: #eee; } + +ul.heading_tree li { list-style: none; } +ul.heading_tree { margin: 0; padding-left: 0; } +ul.heading_tree ul { margin: 0; padding-left: 25px; } + diff --git a/conifer/static/xslt/test.xsl b/conifer/static/xslt/test.xsl index b47520c..6f145d8 100644 --- a/conifer/static/xslt/test.xsl +++ b/conifer/static/xslt/test.xsl @@ -1,18 +1,18 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/conifer/syrup/urls.py b/conifer/syrup/urls.py index 0a28079..66c50f5 100644 --- a/conifer/syrup/urls.py +++ b/conifer/syrup/urls.py @@ -1,63 +1,63 @@ -from django.conf.urls.defaults import * - -# I'm not ready to break items out into their own urls.py, but I do -# want to cut down on the common boilerplate in the urlpatterns below. - -ITEM_PREFIX = r'^course/(?P\d+)/item/(?P\d+)/' -GENERIC_REGEX = r'((?P\d+)/)?(?P.+)?$' - -urlpatterns = patterns('conifer.syrup.views', - (r'^$', 'welcome'), - (r'^course/$', 'my_courses'), - (r'^course/new/$', 'add_new_course'), - (r'^course/new/ajax_title$', 'add_new_course_ajax_title'), - (r'^course/invitation/$', 'course_invitation'), - (r'^browse/$', 'browse'), - (r'^browse/(?P.*)/$', 'browse'), - (r'^prefs/$', 'user_prefs'), - (r'^z3950test/$', 'z3950_test'), - #MARK: propose we kill open_courses, we have browse. - (r'^opencourse/$', 'open_courses'), - (r'^search/$', 'search'), - (r'^zsearch/$', 'zsearch'), - #MARK: propose we kill instructors, we have browse - (r'^instructors/$', 'instructors'), - (r'^instructors/search/(?P.*)$', 'instructor_search'), - #MARK: propose we kill departments, we have browse - (r'^departments/$', 'departments'), - (r'^course/(?P\d+)/$', 'course_detail'), - (r'^instructor/(?P.*)/$', 'instructor_detail'), - (r'^department/(?P.*)/$', 'department_detail'), - (r'^course/(?P\d+)/search/$', 'course_search'), - (r'^course/(?P\d+)/edit/$', 'edit_course'), - (r'^course/(?P\d+)/edit/delete/$', 'delete_course'), - (r'^course/(?P\d+)/edit/permission/$', 'edit_course_permissions'), - (r'^course/(?P\d+)/feeds/(?P.*)$', 'course_feeds'), - (r'^course/(?P\d+)/join/$', 'course_join'), - (ITEM_PREFIX + r'$', 'item_detail'), - (ITEM_PREFIX + r'dl/(?P.*)$', 'item_download'), - (ITEM_PREFIX + r'meta$', 'item_metadata'), - (ITEM_PREFIX + r'edit/$', 'item_edit'), - (ITEM_PREFIX + r'delete/$', 'item_delete'), - (ITEM_PREFIX + r'add/$', 'item_add'), # for adding sub-things - (ITEM_PREFIX + r'add/cat_search/$', 'item_add_cat_search'), - - (r'^admin/$', 'admin_index'), - (r'^admin/terms/' + GENERIC_REGEX, 'admin_terms'), - (r'^admin/depts/' + GENERIC_REGEX, 'admin_depts'), - (r'^admin/news/' + GENERIC_REGEX, 'admin_news'), - (r'^admin/targets/' + GENERIC_REGEX, 'admin_targets'), - - (r'^phys/$', 'phys_index'), - (r'^phys/checkout/$', 'phys_checkout'), - (r'^phys/mark_arrived/$', 'phys_mark_arrived'), - (r'^phys/mark_arrived/match/$', 'phys_mark_arrived_match'), - (r'^phys/circlist/$', 'phys_circlist'), - - (r'^course/(?P\d+)/reseq$', 'course_reseq'), - (ITEM_PREFIX + r'reseq', 'item_heading_reseq'), - (ITEM_PREFIX + r'relocate/', 'item_relocate'), # move to new subheading -# (r'^admin/terms/(?P\d+)/$', 'admin_term_edit'), -# (r'^admin/terms/(?P\d+)/delete$', 'admin_term_delete'), -# (r'^admin/terms/$', 'admin_term'), -) +from django.conf.urls.defaults import * + +# I'm not ready to break items out into their own urls.py, but I do +# want to cut down on the common boilerplate in the urlpatterns below. + +ITEM_PREFIX = r'^course/(?P\d+)/item/(?P\d+)/' +GENERIC_REGEX = r'((?P\d+)/)?(?P.+)?$' + +urlpatterns = patterns('conifer.syrup.views', + (r'^$', 'welcome'), + (r'^course/$', 'my_courses'), + (r'^course/new/$', 'add_new_course'), + (r'^course/new/ajax_title$', 'add_new_course_ajax_title'), + (r'^course/invitation/$', 'course_invitation'), + (r'^browse/$', 'browse'), + (r'^browse/(?P.*)/$', 'browse'), + (r'^prefs/$', 'user_prefs'), + (r'^z3950test/$', 'z3950_test'), + #MARK: propose we kill open_courses, we have browse. + (r'^opencourse/$', 'open_courses'), + (r'^search/$', 'search'), + (r'^zsearch/$', 'zsearch'), + #MARK: propose we kill instructors, we have browse + (r'^instructors/$', 'instructors'), + (r'^instructors/search/(?P.*)$', 'instructor_search'), + #MARK: propose we kill departments, we have browse + (r'^departments/$', 'departments'), + (r'^course/(?P\d+)/$', 'course_detail'), + (r'^instructor/(?P.*)/$', 'instructor_detail'), + (r'^department/(?P.*)/$', 'department_detail'), + (r'^course/(?P\d+)/search/$', 'course_search'), + (r'^course/(?P\d+)/edit/$', 'edit_course'), + (r'^course/(?P\d+)/edit/delete/$', 'delete_course'), + (r'^course/(?P\d+)/edit/permission/$', 'edit_course_permissions'), + (r'^course/(?P\d+)/feeds/(?P.*)$', 'course_feeds'), + (r'^course/(?P\d+)/join/$', 'course_join'), + (ITEM_PREFIX + r'$', 'item_detail'), + (ITEM_PREFIX + r'dl/(?P.*)$', 'item_download'), + (ITEM_PREFIX + r'meta$', 'item_metadata'), + (ITEM_PREFIX + r'edit/$', 'item_edit'), + (ITEM_PREFIX + r'delete/$', 'item_delete'), + (ITEM_PREFIX + r'add/$', 'item_add'), # for adding sub-things + (ITEM_PREFIX + r'add/cat_search/$', 'item_add_cat_search'), + + (r'^admin/$', 'admin_index'), + (r'^admin/terms/' + GENERIC_REGEX, 'admin_terms'), + (r'^admin/depts/' + GENERIC_REGEX, 'admin_depts'), + (r'^admin/news/' + GENERIC_REGEX, 'admin_news'), + (r'^admin/targets/' + GENERIC_REGEX, 'admin_targets'), + + (r'^phys/$', 'phys_index'), + (r'^phys/checkout/$', 'phys_checkout'), + (r'^phys/mark_arrived/$', 'phys_mark_arrived'), + (r'^phys/mark_arrived/match/$', 'phys_mark_arrived_match'), + (r'^phys/circlist/$', 'phys_circlist'), + + (r'^course/(?P\d+)/reseq$', 'course_reseq'), + (ITEM_PREFIX + r'reseq', 'item_heading_reseq'), + (ITEM_PREFIX + r'relocate/', 'item_relocate'), # move to new subheading +# (r'^admin/terms/(?P\d+)/$', 'admin_term_edit'), +# (r'^admin/terms/(?P\d+)/delete$', 'admin_term_delete'), +# (r'^admin/terms/$', 'admin_term'), +) diff --git a/conifer/syrup/views/general.py b/conifer/syrup/views/general.py index 633a536..8c49001 100644 --- a/conifer/syrup/views/general.py +++ b/conifer/syrup/views/general.py @@ -1,165 +1,165 @@ -from _common import * -from django.utils.translation import ugettext as _ -from search import * -#from lxml import etree -#import libxml2 -#import libxslt -import os - - -BASE_DIRECTORY = os.path.abspath(os.path.dirname(__file__)) -HERE = lambda s: os.path.join(BASE_DIRECTORY, s) - - -#----------------------------------------------------------------------------- - -def welcome(request): - return g.render('welcome.xhtml') - -# MARK: propose we get rid of this. We already have a 'Courses' browser. -def open_courses(request): - page_num = int(request.GET.get('page', 1)) - count = int(request.GET.get('count', 5)) - paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter? - return g.render('open_courses.xhtml', paginator=paginator, - page_num=page_num, - count=count) -# MARK: propose we drop this too. We have a browse. -def instructors(request): - page_num = int(request.GET.get('page', 1)) - count = int(request.GET.get('count', 5)) - action = request.GET.get('action', 'browse') - if action == 'join': - paginator = Paginator(models.User.active_instructors(), count) - elif action == 'drop': - paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter? - else: - paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter? - - return g.render('instructors.xhtml', paginator=paginator, - page_num=page_num, - count=count) - -def instructor_search(request, instructor): - return search(request, with_instructor=instructor) - -# MARK: propose we get rid of this. We have browse. -def departments(request): - raise NotImplementedError - - -def user_prefs(request): - if request.method != 'POST': - return g.render('prefs.xhtml') - else: - profile = request.user.get_profile() - profile.wants_email_notices = bool(request.POST.get('wants_email_notices')) - profile.save() - return HttpResponseRedirect('../') - -def z3950_test(request): - #z39.50 testing area - - - styledoc = libxml2.parseFile(HERE('../../static/xslt/test.xsl')) - stylexsl = libxslt.parseStylesheetDoc(styledoc) - - #testing JZKitZ3950 - it seems to work, but i have a character set problem - #with the returned marc - #nope - the problem is weak mapping with the limited solr test set - #i think this can be sorted out - - #conn = zoom.Connection ('z3950.loc.gov', 7090) - conn = zoom.Connection ('zed.concat.ca', 210) - print("connecting...") - conn.databaseName = 'OWA' - conn.preferredRecordSyntax = 'XML' - # conn.preferredRecordSyntax = 'USMARC' - query = zoom.Query ('CCL', 'ti="agar"') - res = conn.search (query) - collector = [] - # if we wanted to get into funkiness - m = zmarc.MARC8_to_Unicode () - for r in res: - #print(type(r.data)) - #print(type(m.translate(r.data))) - zhit = str("") + (m.translate(r.data)) - #doc = libxml2.parseDoc(zhit) - #print(stylexsl.applyStylesheet(doc, None)) - - conn.close () - res_str = "" . join(collector) - return g.render('z3950_test.xhtml', res_str=res_str) - -def browse(request, browse_option=''): - #the defaults should be moved into a config file or something... - page_num = int(request.GET.get('page', 1)) - count = int(request.GET.get('count', 5)) - - if browse_option == '': - queryset = None - template = 'browse_index.xhtml' - elif browse_option == 'instructors': - queryset = models.User.active_instructors() - queryset = queryset.filter(user_filters(request.user)['instructors']) - template = 'instructors.xhtml' - elif browse_option == 'departments': - queryset = models.Department.objects.filter(active=True) - template = 'departments.xhtml' - elif browse_option == 'courses': - # fixme, course filter should not be (active=True) but based on user identity. - for_courses = user_filters(request.user)['courses'] - queryset = models.Course.objects.filter(for_courses) - template = 'courses.xhtml' - - queryset = queryset and queryset.distinct() - paginator = Paginator(queryset, count) - return g.render(template, paginator=paginator, - page_num=page_num, - count=count) - -@login_required -def my_courses(request): - return g.render('my_courses.xhtml') - -def instructor_detail(request, instructor_id): - page_num = int(request.GET.get('page', 1)) - count = int(request.GET.get('count', 5)) - ''' - i am not sure this is the best way to go from instructor - to course - ''' - courses = models.Course.objects.filter(member__user=instructor_id, - member__role='INSTR') - filters = user_filters(request.user) - courses = courses.filter(filters['courses']) - paginator = Paginator(courses.order_by('title'), count) - - ''' - no concept of active right now, maybe suppressed is a better - description anyway? - ''' - # filter(active=True).order_by('title'), count) - instructor = models.User.objects.get(pk=instructor_id) - return g.render('courses.xhtml', - custom_title=_('Courses taught by %s') % instructor.get_full_name(), - paginator=paginator, - page_num=page_num, - count=count) - -def department_detail(request, department_id): - page_num = int(request.GET.get('page', 1)) - count = int(request.GET.get('count', 5)) - - paginator = Paginator(models.Course.objects. - filter(department__id=department_id). - order_by('title'), count) - - department = models.Department.objects.get(pk=department_id) - - return g.render('courses.xhtml', - custom_title=_('Courses with Materials in %s') % department.name, - paginator=paginator, - page_num=page_num, - count=count) - +from _common import * +from django.utils.translation import ugettext as _ +from search import * +#from lxml import etree +#import libxml2 +#import libxslt +import os + + +BASE_DIRECTORY = os.path.abspath(os.path.dirname(__file__)) +HERE = lambda s: os.path.join(BASE_DIRECTORY, s) + + +#----------------------------------------------------------------------------- + +def welcome(request): + return g.render('welcome.xhtml') + +# MARK: propose we get rid of this. We already have a 'Courses' browser. +def open_courses(request): + page_num = int(request.GET.get('page', 1)) + count = int(request.GET.get('count', 5)) + paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter? + return g.render('open_courses.xhtml', paginator=paginator, + page_num=page_num, + count=count) +# MARK: propose we drop this too. We have a browse. +def instructors(request): + page_num = int(request.GET.get('page', 1)) + count = int(request.GET.get('count', 5)) + action = request.GET.get('action', 'browse') + if action == 'join': + paginator = Paginator(models.User.active_instructors(), count) + elif action == 'drop': + paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter? + else: + paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter? + + return g.render('instructors.xhtml', paginator=paginator, + page_num=page_num, + count=count) + +def instructor_search(request, instructor): + return search(request, with_instructor=instructor) + +# MARK: propose we get rid of this. We have browse. +def departments(request): + raise NotImplementedError + + +def user_prefs(request): + if request.method != 'POST': + return g.render('prefs.xhtml') + else: + profile = request.user.get_profile() + profile.wants_email_notices = bool(request.POST.get('wants_email_notices')) + profile.save() + return HttpResponseRedirect('../') + +def z3950_test(request): + #z39.50 testing area + + + styledoc = libxml2.parseFile(HERE('../../static/xslt/test.xsl')) + stylexsl = libxslt.parseStylesheetDoc(styledoc) + + #testing JZKitZ3950 - it seems to work, but i have a character set problem + #with the returned marc + #nope - the problem is weak mapping with the limited solr test set + #i think this can be sorted out + + #conn = zoom.Connection ('z3950.loc.gov', 7090) + conn = zoom.Connection ('zed.concat.ca', 210) + print("connecting...") + conn.databaseName = 'OWA' + conn.preferredRecordSyntax = 'XML' + # conn.preferredRecordSyntax = 'USMARC' + query = zoom.Query ('CCL', 'ti="agar"') + res = conn.search (query) + collector = [] + # if we wanted to get into funkiness + m = zmarc.MARC8_to_Unicode () + for r in res: + #print(type(r.data)) + #print(type(m.translate(r.data))) + zhit = str("") + (m.translate(r.data)) + #doc = libxml2.parseDoc(zhit) + #print(stylexsl.applyStylesheet(doc, None)) + + conn.close () + res_str = "" . join(collector) + return g.render('z3950_test.xhtml', res_str=res_str) + +def browse(request, browse_option=''): + #the defaults should be moved into a config file or something... + page_num = int(request.GET.get('page', 1)) + count = int(request.GET.get('count', 5)) + + if browse_option == '': + queryset = None + template = 'browse_index.xhtml' + elif browse_option == 'instructors': + queryset = models.User.active_instructors() + queryset = queryset.filter(user_filters(request.user)['instructors']) + template = 'instructors.xhtml' + elif browse_option == 'departments': + queryset = models.Department.objects.filter(active=True) + template = 'departments.xhtml' + elif browse_option == 'courses': + # fixme, course filter should not be (active=True) but based on user identity. + for_courses = user_filters(request.user)['courses'] + queryset = models.Course.objects.filter(for_courses) + template = 'courses.xhtml' + + queryset = queryset and queryset.distinct() + paginator = Paginator(queryset, count) + return g.render(template, paginator=paginator, + page_num=page_num, + count=count) + +@login_required +def my_courses(request): + return g.render('my_courses.xhtml') + +def instructor_detail(request, instructor_id): + page_num = int(request.GET.get('page', 1)) + count = int(request.GET.get('count', 5)) + ''' + i am not sure this is the best way to go from instructor + to course + ''' + courses = models.Course.objects.filter(member__user=instructor_id, + member__role='INSTR') + filters = user_filters(request.user) + courses = courses.filter(filters['courses']) + paginator = Paginator(courses.order_by('title'), count) + + ''' + no concept of active right now, maybe suppressed is a better + description anyway? + ''' + # filter(active=True).order_by('title'), count) + instructor = models.User.objects.get(pk=instructor_id) + return g.render('courses.xhtml', + custom_title=_('Courses taught by %s') % instructor.get_full_name(), + paginator=paginator, + page_num=page_num, + count=count) + +def department_detail(request, department_id): + page_num = int(request.GET.get('page', 1)) + count = int(request.GET.get('count', 5)) + + paginator = Paginator(models.Course.objects. + filter(department__id=department_id). + order_by('title'), count) + + department = models.Department.objects.get(pk=department_id) + + return g.render('courses.xhtml', + custom_title=_('Courses with Materials in %s') % department.name, + paginator=paginator, + page_num=page_num, + count=count) + diff --git a/conifer/syrup/views/items.py b/conifer/syrup/views/items.py index eac0a21..0533213 100644 --- a/conifer/syrup/views/items.py +++ b/conifer/syrup/views/items.py @@ -1,510 +1,510 @@ -from _common import * -from django.utils.translation import ugettext as _ - -@members_only -def item_detail(request, course_id, item_id): - """Display an item (however that makes sense).""" - # really, displaying an item will vary based on what type of item - # it is -- e.g. a URL item would redirect to the target URL. I'd - # like this URL to be the generic dispatcher, but for now let's - # just display some metadata about the item. - item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) - if item.url: - return _heading_url(request, item) - else: - return item_metadata(request, course_id, item_id) - -@members_only -def item_metadata(request, course_id, item_id): - """Display a metadata page for the item.""" - item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) - if item.item_type == 'HEADING': - return _heading_detail(request, item) - else: - return g.render('item/item_metadata.xhtml', course=item.course, - item=item) - -def _heading_url(request, item): - return HttpResponseRedirect(item.url) - -def _heading_detail(request, item): - """Display a heading. Show the subitems for this heading.""" - return g.render('item/item_heading_detail.xhtml', item=item) - - -@instructors_only -def item_add(request, course_id, item_id): - # The parent_item_id is the id for the parent-heading item. Zero - # represents 'top-level', i.e. the new item should have no - # heading. - #For any other number, we must check that the parent - # item is of the Heading type. - parent_item_id = item_id - if parent_item_id=='0': - parent_item = None - course = get_object_or_404(models.Course, pk=course_id) - siblings = course.item_set.filter(parent_heading=None) - else: - parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id) - assert parent_item.item_type == 'HEADING', _('You can only add items to headings!') - course = parent_item.course - siblings = course.item_set.filter(parent_heading=parent_item) - - try: - next_order = 1 + max(i.sort_order for i in siblings) - except: - next_order = 0 - if not course.can_edit(request.user): - return _access_denied(_('You are not an editor.')) - - item_type = request.GET.get('item_type') - assert item_type, _('No item_type parameter was provided.') - - # for the moment, only HEADINGs, URLs and ELECs can be added. fixme. - assert item_type in ('HEADING', 'URL', 'ELEC', 'PHYS'), \ - _('Sorry, only HEADINGs, URLs and ELECs can be added right now.') - - if request.method != 'POST' and item_type == 'PHYS': - # special handling: send to catalogue search - return HttpResponseRedirect('cat_search/') - - if request.method != 'POST': - item = models.Item() # dummy object - metadata_formset = metadata_formset_class(queryset=item.metadata_set.all()) - return g.render('item/item_add_%s.xhtml' % item_type.lower(), - **locals()) - else: - # fixme, this will need refactoring. But not yet. - author = request.user.get_full_name() or request.user.username - item = models.Item() # dummy object - metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all()) - assert metadata_formset.is_valid() - def do_metadata(item): - for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts - if not obj.get('DELETE'): - item.metadata_set.create(name=obj['name'], value=obj['value']) - - if item_type == 'HEADING': - title = request.POST.get('title', '').strip() - if not title: - # fixme, better error handling. - return HttpResponseRedirect(request.get_full_path()) - else: - item = models.Item( - course=course, - item_type='HEADING', - sort_order = next_order, - parent_heading=parent_item, - title=title, - ) - item.save() - do_metadata(item) - item.save() - elif item_type == 'URL': - title = request.POST.get('title', '').strip() - url = request.POST.get('url', '').strip() - if not (title and url): - # fixme, better error handling. - return HttpResponseRedirect(request.get_full_path()) - else: - item = models.Item( - course=course, - item_type='URL', - parent_heading=parent_item, - sort_order = next_order, - title=title, - url = url) - item.save() - do_metadata(item) - item.save() - elif item_type == 'ELEC': - title = request.POST.get('title', '').strip() - upload = request.FILES.get('file') - if not (title and upload): - # fixme, better error handling. - return HttpResponseRedirect(request.get_full_path()) - item = models.Item( - course=course, - item_type='ELEC', - parent_heading=parent_item, - sort_order = next_order, - title=title, - fileobj_mimetype = upload.content_type, - ) - item.fileobj.save(upload.name, upload) - item.save() - do_metadata(item) - item.save() - else: - raise NotImplementedError - - if parent_item: - return HttpResponseRedirect(parent_item.item_url('meta')) - else: - return HttpResponseRedirect(course.course_url()) - -@instructors_only -def item_add_cat_search(request, course_id, item_id): - # this chunk stolen from item_add(). Refactor. - parent_item_id = item_id - if parent_item_id=='0': - parent_item = None - course = get_object_or_404(models.Course, pk=course_id) - siblings = course.item_set.filter(parent_heading=None) - else: - parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id) - assert parent_item.item_type == 'HEADING', _('You can only add items to headings!') - course = parent_item.course - siblings = course.item_set.filter(parent_heading=parent_item) - - try: - next_order = 1 + max(i.sort_order for i in siblings) - except: - next_order = 0 - - #---------- - - if request.method != 'POST': - if not 'query' in request.GET: - return g.render('item/item_add_cat_search.xhtml', results=[], query='', - course=course, 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) - return g.render('item/item_add_cat_search.xhtml', - results=results, query=query, - start=start, limit=limit, numhits=numhits, - course=course, parent_item=parent_item) - else: - # User has selected an item; add it to course site. - raw_pickitem = request.POST.get('pickitem', '').strip() - #fixme, this block copied from item_add. refactor. - parent_item_id = item_id - if parent_item_id == '0': - # no heading (toplevel) - parent_item = None - course = get_object_or_404(models.Course, pk=course_id) - else: - parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id) - assert parent_item.item_type == 'HEADING', _('You can only add items to headings!') - course = parent_item.course - if not course.can_edit(request.user): - return _access_denied(_('You are not an editor.')) - - pickitem = simplejson.loads(raw_pickitem) - dublin = marcxml_dictionary_to_dc(pickitem) - - # one last thing. If this picked item has an 856$9 field, then - # it's an electronic resource, not a physical item. In that - # case, we add it as a URL, not a PHYS. - if '8569' in pickitem: - dct = dict(item_type='URL', url=pickitem.get('856u')) - else: - dct = dict(item_type='PHYS') - - item = course.item_set.create(parent_heading=parent_item, - sort_order=next_order, - title=dublin.get('dc:title','Untitled'), - **dct) - item.save() - - for dc, value in dublin.items(): - md = item.metadata_set.create(item=item, name=dc, value=value) - # store the whole darn MARC-dict as well (JSON) - item.metadata_set.create(item=item, name='syrup:marc', value=raw_pickitem) - item.save() - return HttpResponseRedirect('../../../%d/meta' % item.id) - -#------------------------------------------------------------ - -#this is used in item_edit. -metadata_formset_class = modelformset_factory(models.Metadata, - fields=['name','value'], - extra=3, can_delete=True) - -@instructors_only -def item_edit(request, course_id, item_id): - course = get_object_or_404(models.Course, pk=course_id) - item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) - item_type = item.item_type - template = 'item/item_add_%s.xhtml' % item_type.lower() - parent_item = item.parent_heading - - if request.method != 'POST': - metadata_formset = metadata_formset_class(queryset=item.metadata_set.all()) - return g.render(template, **locals()) - else: - metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all()) - assert metadata_formset.is_valid() - if 'file' in request.FILES: - # this is a 'replace-current-file' action. - upload = request.FILES.get('file') - item.fileobj.save(upload.name, upload) - item.fileobj_mimetype = upload.content_type - else: - # generally update the item. - [setattr(item, k, v) for (k,v) in request.POST.items()] - # generally update the metadata - item.metadata_set.all().delete() - for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts - if not obj.get('DELETE'): - item.metadata_set.create(name=obj['name'], value=obj['value']) - - item.save() - return HttpResponseRedirect(item.parent_url()) - -@instructors_only -def item_delete(request, course_id, item_id): - course = get_object_or_404(models.Course, pk=course_id) - item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) - if request.method != 'POST': - return g.render('item/item_delete_confirm.xhtml', **locals()) - else: - if 'yes' in request.POST: - # I think Django's ON DELETE CASCADE-like behaviour will - # take care of the sub-items. - if item.parent_heading: - redir = HttpResponseRedirect(item.parent_heading.item_url('meta')) - else: - redir = HttpResponseRedirect(course.course_url()) - item.delete() - return redir - else: - return HttpResponseRedirect('../meta') - -@members_only -def item_download(request, course_id, item_id, filename): - course = get_object_or_404(models.Course, pk=course_id) - item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) - assert item.item_type == 'ELEC', _('Can only download ELEC documents!') - fileiter = item.fileobj.chunks() - resp = HttpResponse(fileiter) - resp['Content-Type'] = item.fileobj_mimetype or 'application/octet-stream' - #resp['Content-Disposition'] = 'attachment; filename=%s' % name - return resp - - - -#------------------------------------------------------------ -# resequencing items - -def _reseq(request, course, parent_heading): - new_order = request.POST['new_order'].strip().split(' ') - # new_order is now a list like this: ['item_3', 'item_8', 'item_1', ...]. - # get at the ints. - new_order = [int(n.split('_')[1]) for n in new_order] - print >> sys.stderr, new_order - the_items = list(course.item_set.filter(parent_heading=parent_heading).order_by('sort_order')) - # sort the items by position in new_order - the_items.sort(key=lambda item: new_order.index(item.id)) - for newnum, item in enumerate(the_items): - item.sort_order = newnum - item.save() - return HttpResponse("'ok'"); - -@instructors_only -def course_reseq(request, course_id): - course = get_object_or_404(models.Course, pk=course_id) - parent_heading = None - return _reseq(request, course, parent_heading) - -@instructors_only -def item_heading_reseq(request, course_id, item_id): - course = get_object_or_404(models.Course, pk=course_id) - item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) - parent_heading = item - return _reseq(request, course, parent_heading) - - -@instructors_only -def item_relocate(request, course_id, item_id): - """Move an item from its current subheading to another one.""" - course = get_object_or_404(models.Course, pk=course_id) - item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) - if request.method != 'POST': - return g.render('item/item_relocate.xhtml', **locals()) - else: - newheading = int(request.POST['heading']) - if newheading == 0: - new_parent = None - else: - new_parent = course.item_set.get(pk=newheading) - if item in new_parent.hierarchy(): - # then we would create a cycle. Bail out. - return simple_message(_('Impossible item-move!'), - _('You cannot make an item a descendant of itself!')) - item.parent_heading = new_parent - item.save() - if new_parent: - return HttpResponseRedirect(new_parent.item_url('meta')) - else: - return HttpResponseRedirect(course.course_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', course__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) - - +from _common import * +from django.utils.translation import ugettext as _ + +@members_only +def item_detail(request, course_id, item_id): + """Display an item (however that makes sense).""" + # really, displaying an item will vary based on what type of item + # it is -- e.g. a URL item would redirect to the target URL. I'd + # like this URL to be the generic dispatcher, but for now let's + # just display some metadata about the item. + item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) + if item.url: + return _heading_url(request, item) + else: + return item_metadata(request, course_id, item_id) + +@members_only +def item_metadata(request, course_id, item_id): + """Display a metadata page for the item.""" + item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) + if item.item_type == 'HEADING': + return _heading_detail(request, item) + else: + return g.render('item/item_metadata.xhtml', course=item.course, + item=item) + +def _heading_url(request, item): + return HttpResponseRedirect(item.url) + +def _heading_detail(request, item): + """Display a heading. Show the subitems for this heading.""" + return g.render('item/item_heading_detail.xhtml', item=item) + + +@instructors_only +def item_add(request, course_id, item_id): + # The parent_item_id is the id for the parent-heading item. Zero + # represents 'top-level', i.e. the new item should have no + # heading. + #For any other number, we must check that the parent + # item is of the Heading type. + parent_item_id = item_id + if parent_item_id=='0': + parent_item = None + course = get_object_or_404(models.Course, pk=course_id) + siblings = course.item_set.filter(parent_heading=None) + else: + parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id) + assert parent_item.item_type == 'HEADING', _('You can only add items to headings!') + course = parent_item.course + siblings = course.item_set.filter(parent_heading=parent_item) + + try: + next_order = 1 + max(i.sort_order for i in siblings) + except: + next_order = 0 + if not course.can_edit(request.user): + return _access_denied(_('You are not an editor.')) + + item_type = request.GET.get('item_type') + assert item_type, _('No item_type parameter was provided.') + + # for the moment, only HEADINGs, URLs and ELECs can be added. fixme. + assert item_type in ('HEADING', 'URL', 'ELEC', 'PHYS'), \ + _('Sorry, only HEADINGs, URLs and ELECs can be added right now.') + + if request.method != 'POST' and item_type == 'PHYS': + # special handling: send to catalogue search + return HttpResponseRedirect('cat_search/') + + if request.method != 'POST': + item = models.Item() # dummy object + metadata_formset = metadata_formset_class(queryset=item.metadata_set.all()) + return g.render('item/item_add_%s.xhtml' % item_type.lower(), + **locals()) + else: + # fixme, this will need refactoring. But not yet. + author = request.user.get_full_name() or request.user.username + item = models.Item() # dummy object + metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all()) + assert metadata_formset.is_valid() + def do_metadata(item): + for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts + if not obj.get('DELETE'): + item.metadata_set.create(name=obj['name'], value=obj['value']) + + if item_type == 'HEADING': + title = request.POST.get('title', '').strip() + if not title: + # fixme, better error handling. + return HttpResponseRedirect(request.get_full_path()) + else: + item = models.Item( + course=course, + item_type='HEADING', + sort_order = next_order, + parent_heading=parent_item, + title=title, + ) + item.save() + do_metadata(item) + item.save() + elif item_type == 'URL': + title = request.POST.get('title', '').strip() + url = request.POST.get('url', '').strip() + if not (title and url): + # fixme, better error handling. + return HttpResponseRedirect(request.get_full_path()) + else: + item = models.Item( + course=course, + item_type='URL', + parent_heading=parent_item, + sort_order = next_order, + title=title, + url = url) + item.save() + do_metadata(item) + item.save() + elif item_type == 'ELEC': + title = request.POST.get('title', '').strip() + upload = request.FILES.get('file') + if not (title and upload): + # fixme, better error handling. + return HttpResponseRedirect(request.get_full_path()) + item = models.Item( + course=course, + item_type='ELEC', + parent_heading=parent_item, + sort_order = next_order, + title=title, + fileobj_mimetype = upload.content_type, + ) + item.fileobj.save(upload.name, upload) + item.save() + do_metadata(item) + item.save() + else: + raise NotImplementedError + + if parent_item: + return HttpResponseRedirect(parent_item.item_url('meta')) + else: + return HttpResponseRedirect(course.course_url()) + +@instructors_only +def item_add_cat_search(request, course_id, item_id): + # this chunk stolen from item_add(). Refactor. + parent_item_id = item_id + if parent_item_id=='0': + parent_item = None + course = get_object_or_404(models.Course, pk=course_id) + siblings = course.item_set.filter(parent_heading=None) + else: + parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id) + assert parent_item.item_type == 'HEADING', _('You can only add items to headings!') + course = parent_item.course + siblings = course.item_set.filter(parent_heading=parent_item) + + try: + next_order = 1 + max(i.sort_order for i in siblings) + except: + next_order = 0 + + #---------- + + if request.method != 'POST': + if not 'query' in request.GET: + return g.render('item/item_add_cat_search.xhtml', results=[], query='', + course=course, 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) + return g.render('item/item_add_cat_search.xhtml', + results=results, query=query, + start=start, limit=limit, numhits=numhits, + course=course, parent_item=parent_item) + else: + # User has selected an item; add it to course site. + raw_pickitem = request.POST.get('pickitem', '').strip() + #fixme, this block copied from item_add. refactor. + parent_item_id = item_id + if parent_item_id == '0': + # no heading (toplevel) + parent_item = None + course = get_object_or_404(models.Course, pk=course_id) + else: + parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id) + assert parent_item.item_type == 'HEADING', _('You can only add items to headings!') + course = parent_item.course + if not course.can_edit(request.user): + return _access_denied(_('You are not an editor.')) + + pickitem = simplejson.loads(raw_pickitem) + dublin = marcxml_dictionary_to_dc(pickitem) + + # one last thing. If this picked item has an 856$9 field, then + # it's an electronic resource, not a physical item. In that + # case, we add it as a URL, not a PHYS. + if '8569' in pickitem: + dct = dict(item_type='URL', url=pickitem.get('856u')) + else: + dct = dict(item_type='PHYS') + + item = course.item_set.create(parent_heading=parent_item, + sort_order=next_order, + title=dublin.get('dc:title','Untitled'), + **dct) + item.save() + + for dc, value in dublin.items(): + md = item.metadata_set.create(item=item, name=dc, value=value) + # store the whole darn MARC-dict as well (JSON) + item.metadata_set.create(item=item, name='syrup:marc', value=raw_pickitem) + item.save() + return HttpResponseRedirect('../../../%d/meta' % item.id) + +#------------------------------------------------------------ + +#this is used in item_edit. +metadata_formset_class = modelformset_factory(models.Metadata, + fields=['name','value'], + extra=3, can_delete=True) + +@instructors_only +def item_edit(request, course_id, item_id): + course = get_object_or_404(models.Course, pk=course_id) + item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) + item_type = item.item_type + template = 'item/item_add_%s.xhtml' % item_type.lower() + parent_item = item.parent_heading + + if request.method != 'POST': + metadata_formset = metadata_formset_class(queryset=item.metadata_set.all()) + return g.render(template, **locals()) + else: + metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all()) + assert metadata_formset.is_valid() + if 'file' in request.FILES: + # this is a 'replace-current-file' action. + upload = request.FILES.get('file') + item.fileobj.save(upload.name, upload) + item.fileobj_mimetype = upload.content_type + else: + # generally update the item. + [setattr(item, k, v) for (k,v) in request.POST.items()] + # generally update the metadata + item.metadata_set.all().delete() + for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts + if not obj.get('DELETE'): + item.metadata_set.create(name=obj['name'], value=obj['value']) + + item.save() + return HttpResponseRedirect(item.parent_url()) + +@instructors_only +def item_delete(request, course_id, item_id): + course = get_object_or_404(models.Course, pk=course_id) + item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) + if request.method != 'POST': + return g.render('item/item_delete_confirm.xhtml', **locals()) + else: + if 'yes' in request.POST: + # I think Django's ON DELETE CASCADE-like behaviour will + # take care of the sub-items. + if item.parent_heading: + redir = HttpResponseRedirect(item.parent_heading.item_url('meta')) + else: + redir = HttpResponseRedirect(course.course_url()) + item.delete() + return redir + else: + return HttpResponseRedirect('../meta') + +@members_only +def item_download(request, course_id, item_id, filename): + course = get_object_or_404(models.Course, pk=course_id) + item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) + assert item.item_type == 'ELEC', _('Can only download ELEC documents!') + fileiter = item.fileobj.chunks() + resp = HttpResponse(fileiter) + resp['Content-Type'] = item.fileobj_mimetype or 'application/octet-stream' + #resp['Content-Disposition'] = 'attachment; filename=%s' % name + return resp + + + +#------------------------------------------------------------ +# resequencing items + +def _reseq(request, course, parent_heading): + new_order = request.POST['new_order'].strip().split(' ') + # new_order is now a list like this: ['item_3', 'item_8', 'item_1', ...]. + # get at the ints. + new_order = [int(n.split('_')[1]) for n in new_order] + print >> sys.stderr, new_order + the_items = list(course.item_set.filter(parent_heading=parent_heading).order_by('sort_order')) + # sort the items by position in new_order + the_items.sort(key=lambda item: new_order.index(item.id)) + for newnum, item in enumerate(the_items): + item.sort_order = newnum + item.save() + return HttpResponse("'ok'"); + +@instructors_only +def course_reseq(request, course_id): + course = get_object_or_404(models.Course, pk=course_id) + parent_heading = None + return _reseq(request, course, parent_heading) + +@instructors_only +def item_heading_reseq(request, course_id, item_id): + course = get_object_or_404(models.Course, pk=course_id) + item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) + parent_heading = item + return _reseq(request, course, parent_heading) + + +@instructors_only +def item_relocate(request, course_id, item_id): + """Move an item from its current subheading to another one.""" + course = get_object_or_404(models.Course, pk=course_id) + item = get_object_or_404(models.Item, pk=item_id, course__id=course_id) + if request.method != 'POST': + return g.render('item/item_relocate.xhtml', **locals()) + else: + newheading = int(request.POST['heading']) + if newheading == 0: + new_parent = None + else: + new_parent = course.item_set.get(pk=newheading) + if item in new_parent.hierarchy(): + # then we would create a cycle. Bail out. + return simple_message(_('Impossible item-move!'), + _('You cannot make an item a descendant of itself!')) + item.parent_heading = new_parent + item.save() + if new_parent: + return HttpResponseRedirect(new_parent.item_url('meta')) + else: + return HttpResponseRedirect(course.course_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', course__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/departments.xhtml b/conifer/templates/departments.xhtml index 5a3673a..c87711f 100644 --- a/conifer/templates/departments.xhtml +++ b/conifer/templates/departments.xhtml @@ -1,26 +1,26 @@ - - - - - - ${title} - - - -

${title}

- - Department - - - ${department.name} - - ${pagetable(paginator, count, pagerow, pageheader)} - - + + + + + + ${title} + + + +

${title}

+ + Department + + + ${department.name} + + ${pagetable(paginator, count, pagerow, pageheader)} + + diff --git a/conifer/templates/edit_course.xhtml b/conifer/templates/edit_course.xhtml index 03dc2bf..8c6c1de 100644 --- a/conifer/templates/edit_course.xhtml +++ b/conifer/templates/edit_course.xhtml @@ -1,58 +1,58 @@ - - - - - - ${title} - - - - -
${course_banner(instance)}
-

${title}

-

Edit course permissionsReturn to course page

-
- - ${field.label} - -
    -
  • ${err}
  • -
- ${Markup(field)} - - e.g., ${example} - -

General description

- - ${field_row(form.code, example)} - ${field_row(form.title)} - ${field_row(form.term)} - ${field_row(form.department)} - - -

${go_back_link()}

-
-
-
-

Delete this course

-
-

- -

-

${go_back_link()}

-
-
- - + + + + + + ${title} + + + + +
${course_banner(instance)}
+

${title}

+

Edit course permissionsReturn to course page

+
+ + ${field.label} + +
    +
  • ${err}
  • +
+ ${Markup(field)} + + e.g., ${example} + +

General description

+ + ${field_row(form.code, example)} + ${field_row(form.title)} + ${field_row(form.term)} + ${field_row(form.department)} + + +

${go_back_link()}

+
+
+
+

Delete this course

+
+

+ +

+

${go_back_link()}

+
+
+ + diff --git a/conifer/templates/item/item_add_cat_search.xhtml b/conifer/templates/item/item_add_cat_search.xhtml index 90061e7..865ad4c 100644 --- a/conifer/templates/item/item_add_cat_search.xhtml +++ b/conifer/templates/item/item_add_cat_search.xhtml @@ -1,89 +1,89 @@ - - - - - - - ${title} - - - - - ${course_banner(course)} - ${nested_title(parent_item)} -

${title}

-
- ${helptext} -
- -
- - - ${go_back_link()} - -
-
-

- ${start}–${min(numhits, start+limit-1)} of ${numhits} results. - - Previous ${limit} - • - - - Next ${limit} - -

-
- ${page_control()} - - - - - - - - - - - - - - - -
#TitleAuthorPublisherPubDate
${resultnum+start}. - ${dc.get('dc:title', '???')} - details -

- Electronic resource. view -

-
${dc.get(k) or '—'} -
- - -
-
- ${page_control()} - - + + + + + + + ${title} + + + + + ${course_banner(course)} + ${nested_title(parent_item)} +

${title}

+
+ ${helptext} +
+ +
+ + + ${go_back_link()} + +
+
+

+ ${start}–${min(numhits, start+limit-1)} of ${numhits} results. + + Previous ${limit} + • + + + Next ${limit} + +

+
+ ${page_control()} + + + + + + + + + + + + + + + +
#TitleAuthorPublisherPubDate
${resultnum+start}. + ${dc.get('dc:title', '???')} + details +

+ Electronic resource. view +

+
${dc.get(k) or '—'} +
+ + +
+
+ ${page_control()} + + diff --git a/conifer/templates/master.xhtml b/conifer/templates/master.xhtml index ba1506d..7f36382 100644 --- a/conifer/templates/master.xhtml +++ b/conifer/templates/master.xhtml @@ -1,79 +1,79 @@ - - - - - ${app_name}<py:if test="t">: ${t}</py:if> - - -