[open-ils-commits] r762 - in servres/trunk/conifer: . custom libsystems/sip libsystems/z3950 static static/jquery/js static/xslt syrup syrup/views templates templates/item (gfawcett)

svn at svn.open-ils.org svn at svn.open-ils.org
Thu Jan 21 21:48:04 EST 2010


Author: gfawcett
Date: 2010-01-21 21:48:02 -0500 (Thu, 21 Jan 2010)
New Revision: 762

Modified:
   servres/trunk/conifer/README
   servres/trunk/conifer/custom/course_codes.py
   servres/trunk/conifer/custom/lib_integration.py
   servres/trunk/conifer/libsystems/sip/sipclient.py
   servres/trunk/conifer/libsystems/z3950/pyz3950_search.py
   servres/trunk/conifer/settings.py
   servres/trunk/conifer/static/edit_course.js
   servres/trunk/conifer/static/jquery/js/jquery-ui-1.7.1.custom.min.js
   servres/trunk/conifer/static/jquery/js/jquery.js
   servres/trunk/conifer/static/main.css
   servres/trunk/conifer/static/xslt/test.xsl
   servres/trunk/conifer/syrup/urls.py
   servres/trunk/conifer/syrup/views/general.py
   servres/trunk/conifer/syrup/views/items.py
   servres/trunk/conifer/templates/departments.xhtml
   servres/trunk/conifer/templates/edit_course.xhtml
   servres/trunk/conifer/templates/item/item_add_cat_search.xhtml
   servres/trunk/conifer/templates/master.xhtml
Log:
stripped out pesky Windows carriage returns

Modified: servres/trunk/conifer/README
===================================================================
--- servres/trunk/conifer/README	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/README	2010-01-22 02:48:02 UTC (rev 762)
@@ -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 <artrhyno at uwindsor.ca>
-Graham Fawcett <graham.fawcett at gmail.com>
-
-
-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 <artrhyno at uwindsor.ca>
+Graham Fawcett <graham.fawcett at gmail.com>
+
+
+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

Modified: servres/trunk/conifer/custom/course_codes.py
===================================================================
--- servres/trunk/conifer/custom/course_codes.py	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/custom/course_codes.py	2010-01-22 02:48:02 UTC (rev 762)
@@ -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]))
+

Modified: servres/trunk/conifer/custom/lib_integration.py
===================================================================
--- servres/trunk/conifer/custom/lib_integration.py	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/custom/lib_integration.py	2010-01-22 02:48:02 UTC (rev 762)
@@ -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
-
-
- at caching('patroninfo', timeout=300)
- at SIP
-def patron_info(conn, barcode):
-    return conn.patron_info(barcode)
-
- at caching('itemstatus', timeout=300)
- at SIP
-def item_status(conn, barcode):
-    return conn.item_info(barcode)
-
- at SIP
-def checkout(conn, patron_barcode, item_barcode):
-    return conn.checkout(patron_barcode, item_barcode, '')
-
- at SIP
-def checkin(conn, item_barcode):
-    return conn.checkin(item_barcode, institution='', location='')
-
-
- at caching('bcbi', timeout=3600)
-def barcode_to_bib_id(barcode):
-    return I.barcode_to_bib_id(barcode)
-
- at caching('bccp', timeout=3600)
-def barcode_to_copy(barcode):
-    return I.barcode_to_copy(barcode)
-
- at 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
+
+
+ at caching('patroninfo', timeout=300)
+ at SIP
+def patron_info(conn, barcode):
+    return conn.patron_info(barcode)
+
+ at caching('itemstatus', timeout=300)
+ at SIP
+def item_status(conn, barcode):
+    return conn.item_info(barcode)
+
+ at SIP
+def checkout(conn, patron_barcode, item_barcode):
+    return conn.checkout(patron_barcode, item_barcode, '')
+
+ at SIP
+def checkin(conn, item_barcode):
+    return conn.checkin(item_barcode, institution='', location='')
+
+
+ at caching('bcbi', timeout=3600)
+def barcode_to_bib_id(barcode):
+    return I.barcode_to_bib_id(barcode)
+
+ at caching('bccp', timeout=3600)
+def barcode_to_copy(barcode):
+    return I.barcode_to_copy(barcode)
+
+ at 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

Modified: servres/trunk/conifer/libsystems/sip/sipclient.py
===================================================================
--- servres/trunk/conifer/libsystems/sip/sipclient.py	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/libsystems/sip/sipclient.py	2010-01-22 02:48:02 UTC (rev 762)
@@ -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'}))
+
+

Modified: servres/trunk/conifer/libsystems/z3950/pyz3950_search.py
===================================================================
--- servres/trunk/conifer/libsystems/z3950/pyz3950_search.py	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/libsystems/z3950/pyz3950_search.py	2010-01-22 02:48:02 UTC (rev 762)
@@ -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('<record .*?</record>', 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('<record .*?</record>', 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))

Modified: servres/trunk/conifer/settings.py
===================================================================
--- servres/trunk/conifer/settings.py	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/settings.py	2010-01-22 02:48:02 UTC (rev 762)
@@ -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 at 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 at 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 at 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 at 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:///'

Modified: servres/trunk/conifer/static/edit_course.js
===================================================================
--- servres/trunk/conifer/static/edit_course.js	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/static/edit_course.js	2010-01-22 02:48:02 UTC (rev 762)
@@ -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);

Modified: servres/trunk/conifer/static/jquery/js/jquery-ui-1.7.1.custom.min.js
===================================================================
--- servres/trunk/conifer/static/jquery/js/jquery-ui-1.7.1.custom.min.js	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/static/jquery/js/jquery-ui-1.7.1.custom.min.js	2010-01-22 02:48:02 UTC (rev 762)
@@ -1,47 +1,47 @@
-/*
- * 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 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;m<n.length;m++){if(j.options[n[m][0]]){n[m][1].apply(j.element,k)}}}},contains:function(k,j){return document.compareDocumentPosition?k.compareDocumentPosition(j)&16:k!==j&&k.contains(j)},hasScroll:function(m,k){if(c(m).css("overflow")=="hidden"){return false}var j=(k&&k=="left")?"scrollLeft":"scrollTop",l=false;if(m[j]>0){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
+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;m<n.length;m++){if(j.options[n[m][0]]){n[m][1].apply(j.element,k)}}}},contains:function(k,j){return document.compareDocumentPosition?k.compareDocumentPosition(j)&16:k!==j&&k.contains(j)},hasScroll:function(m,k){if(c(m).css("overflow")=="hidden"){return false}var j=(k&&k=="left")?"scrollLeft":"scrollTop",l=false;if(m[j]>0){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.left<this.containment[0]){d=this.containment[0]+this.offset.click.left}if(e.pageY-this.offset.click.top<this.containment[1]){c=this.containment[1]+this.offset.click.top}if(e.pageX-this.offset.click.left>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.top<this.containment[1]||g-this.offset.click.top>this.containment[3])?g:(!(g-this.offset.click.top<this.containment[1])?g-h.grid[1]:g+h.grid[1])):g;var f=this.originalPageX+Math.round((d-this.originalPageX)/h.grid[0])*h.grid[0];d=this.containment?(!(f-this.offset.click.left<this.containment[0]||f-this.offset.click.left>this.containment[2])?f:(!(f-this.offset.click.left<this.containment[0])?f-h.grid[0]:f+h.grid[0])):f}}return{top:(c-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(i?0:b.scrollTop())))),left:(d-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():i?0:b.scrollLeft())))}},_clear:function(){this.helper.removeClass("ui-draggable-dragging");if(this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval){this.helper.remove()}this.helper=null;this.cancelHelperRemoval=false},_trigger:function(b,c,d){d=d||this._uiHash();a.ui.plugin.call(this,b,[c,d]);if(b=="drag"){this.positionAbs=this._convertPositionTo("absolute")}return a.widget.prototype._trigger.call(this,b,c,d)},plugins:{},_uiHash:function(b){return{helper:this.helper,position:this.position,absolutePosition:this.positionAbs,offset:this.positionAbs}}}));a.extend(a.ui.draggable,{version:"1.7.1",eventPrefix:"drag",defaults:{addClasses:true,appendTo:"parent",axis:false,cancel:":input,option",connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,delay:0,distance:1,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false}});a.ui.plugin.add("draggable","connectToSortable",{start:function(c,e){var d=a(this).data("draggable"),f=d.options,b=a.extend({},e,{item:d.element});d.sortables=[];a(f.connectToSortable).each(function(){var g=a.data(this,"sortable");if(g&&!g.options.disabled){d.sortables.push({instance:g,shouldRevert:g.options.revert});g._refreshItems();g._trigger("activate",c,b)}})},stop:function(c,e){var d=a(this).data("draggable"),b=a.extend({},e,{item:d.element});a.each(d.sortables,function(){if(this.instance.isOver){this.instance.isOver=0;d.cancelHelperRemoval=true;this.instance.cancelHelperRemoval=false;if(this.shouldRevert){this.instance.options.revert=true}this.instance._mouseStop(c);this.instance.options.helper=this.instance.options._helper;if(d.options.helper=="original"){this.instance.currentItem.css({top:"auto",left:"auto"})}}else{this.instance.cancelHelperRemoval=false;this.instance._trigger("deactivate",c,b)}})},drag:function(c,f){var e=a(this).data("draggable"),b=this;var d=function(i){var n=this.offset.click.top,m=this.offset.click.left;var g=this.positionAbs.top,k=this.positionAbs.left;var j=i.height,l=i.width;var p=i.top,h=i.left;return a.ui.isOver(g+n,k+m,p,h,j,l)};a.each(e.sortables,function(g){this.instance.positionAbs=e.positionAbs;this.instance.helperProportions=e.helperProportions;this.instance.offset.click=e.offset.click;if(this.instance._intersectsWith(this.instance.containerCache)){if(!this.instance.isOver){this.instance.isOver=1;this.instance.currentItem=a(b).clone().appendTo(this.instance.element).data("sortable-item",true);this.instance.options._helper=this.instance.options.helper;this.instance.options.helper=function(){return f.helper[0]};c.target=this.instance.currentItem[0];this.instance._mouseCapture(c,true);this.instance._mouseStart(c,true,true);this.instance.offset.click.top=e.offset.click.top;this.instance.offset.click.left=e.offset.click.left;this.instance.offset.parent.left-=e.offset.parent.left-this.instance.offset.parent.left;this.instance.offset.parent.top-=e.offset.parent.top-this.instance.offset.parent.top;e._trigger("toSortable",c);e.dropped=this.instance.element;e.currentItem=e.element;this.instance.fromOutside=e}if(this.instance.currentItem){this.instance._mouseDrag(c)}}else{if(this.instance.isOver){this.instance.isOver=0;this.instance.cancelHelperRemoval=true;this.instance.options.revert=false;this.instance._trigger("out",c,this.instance._uiHash(this.instance));this.instance._mouseStop(c,true);this.instance.options.helper=this.instance.options._helper;this.instance.currentItem.remove();if(this.instance.placeholder){this.instance.placeholder.remove()}e._trigger("fromSortable",c);e.dropped=false}}})}});a.ui.plugin.add("draggable","cursor",{start:function(c,d){var b=a("body"),e=a(this).data("draggable").options;if(b.css("cursor")){e._cursor=b.css("cursor")}b.css("cursor",e.cursor)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._cursor){a("body").css("cursor",d._cursor)}}});a.ui.plugin.add("draggable","iframeFix",{start:function(b,c){var d=a(this).data("draggable").options;a(d.iframeFix===true?"iframe":d.iframeFix).each(function(){a('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').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<f.scrollSensitivity){c.scrollParent[0].scrollTop=b=c.scrollParent[0].scrollTop+f.scrollSpeed}else{if(d.pageY-c.overflowOffset.top<f.scrollSensitivity){c.scrollParent[0].scrollTop=b=c.scrollParent[0].scrollTop-f.scrollSpeed}}}if(!f.axis||f.axis!="y"){if((c.overflowOffset.left+c.scrollParent[0].offsetWidth)-d.pageX<f.scrollSensitivity){c.scrollParent[0].scrollLeft=b=c.scrollParent[0].scrollLeft+f.scrollSpeed}else{if(d.pageX-c.overflowOffset.left<f.scrollSensitivity){c.scrollParent[0].scrollLeft=b=c.scrollParent[0].scrollLeft-f.scrollSpeed}}}}else{if(!f.axis||f.axis!="x"){if(d.pageY-a(document).scrollTop()<f.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()-f.scrollSpeed)}else{if(a(window).height()-(d.pageY-a(document).scrollTop())<f.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()+f.scrollSpeed)}}}if(!f.axis||f.axis!="y"){if(d.pageX-a(document).scrollLeft()<f.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()-f.scrollSpeed)}else{if(a(window).width()-(d.pageX-a(document).scrollLeft())<f.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()+f.scrollSpeed)}}}}if(b!==false&&a.ui.ddmanager&&!f.dropBehaviour){a.ui.ddmanager.prepareOffsets(c,d)}}});a.ui.plugin.add("draggable","snap",{start:function(c,d){var b=a(this).data("draggable"),e=b.options;b.snapElements=[];a(e.snap.constructor!=String?(e.snap.items||":data(draggable)"):e.snap).each(function(){var g=a(this);var f=g.offset();if(this!=b.element[0]){b.snapElements.push({item:this,width:g.outerWidth(),height:g.outerHeight(),top:f.top,left:f.left})}})},drag:function(u,p){var g=a(this).data("draggable"),q=g.options;var y=q.snapTolerance;var x=p.offset.left,w=x+g.helperProportions.width,f=p.offset.top,e=f+g.helperProportions.height;for(var v=g.snapElements.length-1;v>=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<x&&x<n+y&&m-y<f&&f<A+y)||(s-y<x&&x<n+y&&m-y<e&&e<A+y)||(s-y<w&&w<n+y&&m-y<f&&f<A+y)||(s-y<w&&w<n+y&&m-y<e&&e<A+y))){if(g.snapElements[v].snapping){(g.options.snap.release&&g.options.snap.release.call(g.element,u,a.extend(g._uiHash(),{snapItem:g.snapElements[v].item})))}g.snapElements[v].snapping=false;continue}if(q.snapMode!="inner"){var c=Math.abs(m-e)<=y;var z=Math.abs(A-f)<=y;var j=Math.abs(s-w)<=y;var k=Math.abs(n-x)<=y;if(c){p.position.top=g._convertPositionTo("relative",{top:m-g.helperProportions.height,left:0}).top-g.margins.top}if(z){p.position.top=g._convertPositionTo("relative",{top:A,left:0}).top-g.margins.top}if(j){p.position.left=g._convertPositionTo("relative",{top:0,left:s-g.helperProportions.width}).left-g.margins.left}if(k){p.position.left=g._convertPositionTo("relative",{top:0,left:n}).left-g.margins.left}}var h=(c||z||j||k);if(q.snapMode!="outer"){var c=Math.abs(m-f)<=y;var z=Math.abs(A-e)<=y;var j=Math.abs(s-x)<=y;var k=Math.abs(n-w)<=y;if(c){p.position.top=g._convertPositionTo("relative",{top:m,left:0}).top-g.margins.top}if(z){p.position.top=g._convertPositionTo("relative",{top:A-g.helperProportions.height,left:0}).top-g.margins.top}if(j){p.position.left=g._convertPositionTo("relative",{top:0,left:s}).left-g.margins.left}if(k){p.position.left=g._convertPositionTo("relative",{top:0,left:n-g.helperProportions.width}).left-g.margins.left}}if(!g.snapElements[v].snapping&&(c||z||j||k||h)){(g.options.snap.snap&&g.options.snap.snap.call(g.element,u,a.extend(g._uiHash(),{snapItem:g.snapElements[v].item})))}g.snapElements[v].snapping=(c||z||j||k||h)}}});a.ui.plugin.add("draggable","stack",{start:function(b,c){var e=a(this).data("draggable").options;var d=a.makeArray(a(e.stack.group)).sort(function(g,f){return(parseInt(a(g).css("zIndex"),10)||e.stack.min)-(parseInt(a(f).css("zIndex"),10)||e.stack.min)});a(d).each(function(f){this.style.zIndex=e.stack.min+f});this[0].style.zIndex=e.stack.min+d.length}});a.ui.plugin.add("draggable","zIndex",{start:function(c,d){var b=a(d.helper),e=a(this).data("draggable").options;if(b.css("zIndex")){e._zIndex=b.css("zIndex")}b.css("zIndex",e.zIndex)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._zIndex){a(c.helper).css("zIndex",d._zIndex)}}})})(jQuery);;/*
- * jQuery UI Droppable 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/Droppables
- *
- * Depends:
- *	ui.core.js
- *	ui.draggable.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.left<this.containment[0]){d=this.containment[0]+this.offset.click.left}if(e.pageY-this.offset.click.top<this.containment[1]){c=this.containment[1]+this.offset.click.top}if(e.pageX-this.offset.click.left>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.top<this.containment[1]||g-this.offset.click.top>this.containment[3])?g:(!(g-this.offset.click.top<this.containment[1])?g-h.grid[1]:g+h.grid[1])):g;var f=this.originalPageX+Math.round((d-this.originalPageX)/h.grid[0])*h.grid[0];d=this.containment?(!(f-this.offset.click.left<this.containment[0]||f-this.offset.click.left>this.containment[2])?f:(!(f-this.offset.click.left<this.containment[0])?f-h.grid[0]:f+h.grid[0])):f}}return{top:(c-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(i?0:b.scrollTop())))),left:(d-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():i?0:b.scrollLeft())))}},_clear:function(){this.helper.removeClass("ui-draggable-dragging");if(this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval){this.helper.remove()}this.helper=null;this.cancelHelperRemoval=false},_trigger:function(b,c,d){d=d||this._uiHash();a.ui.plugin.call(this,b,[c,d]);if(b=="drag"){this.positionAbs=this._convertPositionTo("absolute")}return a.widget.prototype._trigger.call(this,b,c,d)},plugins:{},_uiHash:function(b){return{helper:this.helper,position:this.position,absolutePosition:this.positionAbs,offset:this.positionAbs}}}));a.extend(a.ui.draggable,{version:"1.7.1",eventPrefix:"drag",defaults:{addClasses:true,appendTo:"parent",axis:false,cancel:":input,option",connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,delay:0,distance:1,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false}});a.ui.plugin.add("draggable","connectToSortable",{start:function(c,e){var d=a(this).data("draggable"),f=d.options,b=a.extend({},e,{item:d.element});d.sortables=[];a(f.connectToSortable).each(function(){var g=a.data(this,"sortable");if(g&&!g.options.disabled){d.sortables.push({instance:g,shouldRevert:g.options.revert});g._refreshItems();g._trigger("activate",c,b)}})},stop:function(c,e){var d=a(this).data("draggable"),b=a.extend({},e,{item:d.element});a.each(d.sortables,function(){if(this.instance.isOver){this.instance.isOver=0;d.cancelHelperRemoval=true;this.instance.cancelHelperRemoval=false;if(this.shouldRevert){this.instance.options.revert=true}this.instance._mouseStop(c);this.instance.options.helper=this.instance.options._helper;if(d.options.helper=="original"){this.instance.currentItem.css({top:"auto",left:"auto"})}}else{this.instance.cancelHelperRemoval=false;this.instance._trigger("deactivate",c,b)}})},drag:function(c,f){var e=a(this).data("draggable"),b=this;var d=function(i){var n=this.offset.click.top,m=this.offset.click.left;var g=this.positionAbs.top,k=this.positionAbs.left;var j=i.height,l=i.width;var p=i.top,h=i.left;return a.ui.isOver(g+n,k+m,p,h,j,l)};a.each(e.sortables,function(g){this.instance.positionAbs=e.positionAbs;this.instance.helperProportions=e.helperProportions;this.instance.offset.click=e.offset.click;if(this.instance._intersectsWith(this.instance.containerCache)){if(!this.instance.isOver){this.instance.isOver=1;this.instance.currentItem=a(b).clone().appendTo(this.instance.element).data("sortable-item",true);this.instance.options._helper=this.instance.options.helper;this.instance.options.helper=function(){return f.helper[0]};c.target=this.instance.currentItem[0];this.instance._mouseCapture(c,true);this.instance._mouseStart(c,true,true);this.instance.offset.click.top=e.offset.click.top;this.instance.offset.click.left=e.offset.click.left;this.instance.offset.parent.left-=e.offset.parent.left-this.instance.offset.parent.left;this.instance.offset.parent.top-=e.offset.parent.top-this.instance.offset.parent.top;e._trigger("toSortable",c);e.dropped=this.instance.element;e.currentItem=e.element;this.instance.fromOutside=e}if(this.instance.currentItem){this.instance._mouseDrag(c)}}else{if(this.instance.isOver){this.instance.isOver=0;this.instance.cancelHelperRemoval=true;this.instance.options.revert=false;this.instance._trigger("out",c,this.instance._uiHash(this.instance));this.instance._mouseStop(c,true);this.instance.options.helper=this.instance.options._helper;this.instance.currentItem.remove();if(this.instance.placeholder){this.instance.placeholder.remove()}e._trigger("fromSortable",c);e.dropped=false}}})}});a.ui.plugin.add("draggable","cursor",{start:function(c,d){var b=a("body"),e=a(this).data("draggable").options;if(b.css("cursor")){e._cursor=b.css("cursor")}b.css("cursor",e.cursor)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._cursor){a("body").css("cursor",d._cursor)}}});a.ui.plugin.add("draggable","iframeFix",{start:function(b,c){var d=a(this).data("draggable").options;a(d.iframeFix===true?"iframe":d.iframeFix).each(function(){a('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').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<f.scrollSensitivity){c.scrollParent[0].scrollTop=b=c.scrollParent[0].scrollTop+f.scrollSpeed}else{if(d.pageY-c.overflowOffset.top<f.scrollSensitivity){c.scrollParent[0].scrollTop=b=c.scrollParent[0].scrollTop-f.scrollSpeed}}}if(!f.axis||f.axis!="y"){if((c.overflowOffset.left+c.scrollParent[0].offsetWidth)-d.pageX<f.scrollSensitivity){c.scrollParent[0].scrollLeft=b=c.scrollParent[0].scrollLeft+f.scrollSpeed}else{if(d.pageX-c.overflowOffset.left<f.scrollSensitivity){c.scrollParent[0].scrollLeft=b=c.scrollParent[0].scrollLeft-f.scrollSpeed}}}}else{if(!f.axis||f.axis!="x"){if(d.pageY-a(document).scrollTop()<f.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()-f.scrollSpeed)}else{if(a(window).height()-(d.pageY-a(document).scrollTop())<f.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()+f.scrollSpeed)}}}if(!f.axis||f.axis!="y"){if(d.pageX-a(document).scrollLeft()<f.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()-f.scrollSpeed)}else{if(a(window).width()-(d.pageX-a(document).scrollLeft())<f.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()+f.scrollSpeed)}}}}if(b!==false&&a.ui.ddmanager&&!f.dropBehaviour){a.ui.ddmanager.prepareOffsets(c,d)}}});a.ui.plugin.add("draggable","snap",{start:function(c,d){var b=a(this).data("draggable"),e=b.options;b.snapElements=[];a(e.snap.constructor!=String?(e.snap.items||":data(draggable)"):e.snap).each(function(){var g=a(this);var f=g.offset();if(this!=b.element[0]){b.snapElements.push({item:this,width:g.outerWidth(),height:g.outerHeight(),top:f.top,left:f.left})}})},drag:function(u,p){var g=a(this).data("draggable"),q=g.options;var y=q.snapTolerance;var x=p.offset.left,w=x+g.helperProportions.width,f=p.offset.top,e=f+g.helperProportions.height;for(var v=g.snapElements.length-1;v>=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<x&&x<n+y&&m-y<f&&f<A+y)||(s-y<x&&x<n+y&&m-y<e&&e<A+y)||(s-y<w&&w<n+y&&m-y<f&&f<A+y)||(s-y<w&&w<n+y&&m-y<e&&e<A+y))){if(g.snapElements[v].snapping){(g.options.snap.release&&g.options.snap.release.call(g.element,u,a.extend(g._uiHash(),{snapItem:g.snapElements[v].item})))}g.snapElements[v].snapping=false;continue}if(q.snapMode!="inner"){var c=Math.abs(m-e)<=y;var z=Math.abs(A-f)<=y;var j=Math.abs(s-w)<=y;var k=Math.abs(n-x)<=y;if(c){p.position.top=g._convertPositionTo("relative",{top:m-g.helperProportions.height,left:0}).top-g.margins.top}if(z){p.position.top=g._convertPositionTo("relative",{top:A,left:0}).top-g.margins.top}if(j){p.position.left=g._convertPositionTo("relative",{top:0,left:s-g.helperProportions.width}).left-g.margins.left}if(k){p.position.left=g._convertPositionTo("relative",{top:0,left:n}).left-g.margins.left}}var h=(c||z||j||k);if(q.snapMode!="outer"){var c=Math.abs(m-f)<=y;var z=Math.abs(A-e)<=y;var j=Math.abs(s-x)<=y;var k=Math.abs(n-w)<=y;if(c){p.position.top=g._convertPositionTo("relative",{top:m,left:0}).top-g.margins.top}if(z){p.position.top=g._convertPositionTo("relative",{top:A-g.helperProportions.height,left:0}).top-g.margins.top}if(j){p.position.left=g._convertPositionTo("relative",{top:0,left:s}).left-g.margins.left}if(k){p.position.left=g._convertPositionTo("relative",{top:0,left:n-g.helperProportions.width}).left-g.margins.left}}if(!g.snapElements[v].snapping&&(c||z||j||k||h)){(g.options.snap.snap&&g.options.snap.snap.call(g.element,u,a.extend(g._uiHash(),{snapItem:g.snapElements[v].item})))}g.snapElements[v].snapping=(c||z||j||k||h)}}});a.ui.plugin.add("draggable","stack",{start:function(b,c){var e=a(this).data("draggable").options;var d=a.makeArray(a(e.stack.group)).sort(function(g,f){return(parseInt(a(g).css("zIndex"),10)||e.stack.min)-(parseInt(a(f).css("zIndex"),10)||e.stack.min)});a(d).each(function(f){this.style.zIndex=e.stack.min+f});this[0].style.zIndex=e.stack.min+d.length}});a.ui.plugin.add("draggable","zIndex",{start:function(c,d){var b=a(d.helper),e=a(this).data("draggable").options;if(b.css("zIndex")){e._zIndex=b.css("zIndex")}b.css("zIndex",e.zIndex)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._zIndex){a(c.helper).css("zIndex",d._zIndex)}}})})(jQuery);;/*
+ * jQuery UI Droppable 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/Droppables
+ *
+ * Depends:
+ *	ui.core.js
+ *	ui.draggable.js
  */
-(function(a){a.widget("ui.droppable",{_init:function(){var c=this.options,b=c.accept;this.isover=0;this.isout=1;this.options.accept=this.options.accept&&a.isFunction(this.options.accept)?this.options.accept:function(e){return e.is(b)};this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight};a.ui.ddmanager.droppables[this.options.scope]=a.ui.ddmanager.droppables[this.options.scope]||[];a.ui.ddmanager.droppables[this.options.scope].push(this);(this.options.addClasses&&this.element.addClass("ui-droppable"))},destroy:function(){var b=a.ui.ddmanager.droppables[this.options.scope];for(var c=0;c<b.length;c++){if(b[c]==this){b.splice(c,1)}}this.element.removeClass("ui-droppable ui-droppable-disabled").removeData("droppable").unbind(".droppable")},_setData:function(b,c){if(b=="accept"){this.options.accept=c&&a.isFunction(c)?c:function(e){return e.is(c)}}else{a.widget.prototype._setData.apply(this,arguments)}},_activate:function(c){var b=a.ui.ddmanager.current;if(this.options.activeClass){this.element.addClass(this.options.activeClass)}(b&&this._trigger("activate",c,this.ui(b)))},_deactivate:function(c){var b=a.ui.ddmanager.current;if(this.options.activeClass){this.element.removeClass(this.options.activeClass)}(b&&this._trigger("deactivate",c,this.ui(b)))},_over:function(c){var b=a.ui.ddmanager.current;if(!b||(b.currentItem||b.element)[0]==this.element[0]){return}if(this.options.accept.call(this.element[0],(b.currentItem||b.element))){if(this.options.hoverClass){this.element.addClass(this.options.hoverClass)}this._trigger("over",c,this.ui(b))}},_out:function(c){var b=a.ui.ddmanager.current;if(!b||(b.currentItem||b.element)[0]==this.element[0]){return}if(this.options.accept.call(this.element[0],(b.currentItem||b.element))){if(this.options.hoverClass){this.element.removeClass(this.options.hoverClass)}this._trigger("out",c,this.ui(b))}},_drop:function(c,d){var b=d||a.ui.ddmanager.current;if(!b||(b.currentItem||b.element)[0]==this.element[0]){return false}var e=false;this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function(){var f=a.data(this,"droppable");if(f.options.greedy&&a.ui.intersect(b,a.extend(f,{offset:f.element.offset()}),f.options.tolerance)){e=true;return false}});if(e){return false}if(this.options.accept.call(this.element[0],(b.currentItem||b.element))){if(this.options.activeClass){this.element.removeClass(this.options.activeClass)}if(this.options.hoverClass){this.element.removeClass(this.options.hoverClass)}this._trigger("drop",c,this.ui(b));return this.element}return false},ui:function(b){return{draggable:(b.currentItem||b.element),helper:b.helper,position:b.position,absolutePosition:b.positionAbs,offset:b.positionAbs}}});a.extend(a.ui.droppable,{version:"1.7.1",eventPrefix:"drop",defaults:{accept:"*",activeClass:false,addClasses:true,greedy:false,hoverClass:false,scope:"default",tolerance:"intersect"}});a.ui.intersect=function(q,j,o){if(!j.offset){return false}var e=(q.positionAbs||q.position.absolute).left,d=e+q.helperProportions.width,n=(q.positionAbs||q.position.absolute).top,m=n+q.helperProportions.height;var g=j.offset.left,c=g+j.proportions.width,p=j.offset.top,k=p+j.proportions.height;switch(o){case"fit":return(g<e&&d<c&&p<n&&m<k);break;case"intersect":return(g<e+(q.helperProportions.width/2)&&d-(q.helperProportions.width/2)<c&&p<n+(q.helperProportions.height/2)&&m-(q.helperProportions.height/2)<k);break;case"pointer":var h=((q.positionAbs||q.position.absolute).left+(q.clickOffset||q.offset.click).left),i=((q.positionAbs||q.position.absolute).top+(q.clickOffset||q.offset.click).top),f=a.ui.isOver(i,h,p,g,j.proportions.height,j.proportions.width);return f;break;case"touch":return((n>=p&&n<=k)||(m>=p&&m<=k)||(n<p&&m>k))&&((e>=g&&e<=c)||(d>=g&&d<=c)||(e<g&&d>c));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<b.length;d++){if(b[d].options.disabled||(e&&!b[d].options.accept.call(b[d].element[0],(e.currentItem||e.element)))){continue}for(var c=0;c<h.length;c++){if(h[c]==b[d].element[0]){b[d].proportions.height=0;continue droppablesLoop}}b[d].visible=b[d].element.css("display")!="none";if(!b[d].visible){continue}b[d].offset=b[d].element.offset();b[d].proportions={width:b[d].element[0].offsetWidth,height:b[d].element[0].offsetHeight};if(f=="mousedown"){b[d]._activate.call(b[d],g)}}},drop:function(b,c){var d=false;a.each(a.ui.ddmanager.droppables[b.options.scope],function(){if(!this.options){return}if(!this.options.disabled&&this.visible&&a.ui.intersect(b,this,this.options.tolerance)){d=this._drop.call(this,c)}if(!this.options.disabled&&this.visible&&this.options.accept.call(this.element[0],(b.currentItem||b.element))){this.isout=1;this.isover=0;this._deactivate.call(this,c)}});return d},drag:function(b,c){if(b.options.refreshPositions){a.ui.ddmanager.prepareOffsets(b,c)}a.each(a.ui.ddmanager.droppables[b.options.scope],function(){if(this.options.disabled||this.greedyChild||!this.visible){return}var e=a.ui.intersect(b,this,this.options.tolerance);var g=!e&&this.isover==1?"isout":(e&&this.isover==0?"isover":null);if(!g){return}var f;if(this.options.greedy){var d=this.element.parents(":data(droppable):eq(0)");if(d.length){f=a.data(d[0],"droppable");f.greedyChild=(g=="isover"?1:0)}}if(f&&g=="isover"){f.isover=0;f.isout=1;f._out.call(f,c)}this[g]=1;this[g=="isout"?"isover":"isout"]=0;this[g=="isover"?"_over":"_out"].call(this,c);if(f&&g=="isout"){f.isout=0;f.isover=1;f._over.call(f,c)}})}}})(jQuery);;/*
- * jQuery UI Sortable 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/Sortables
- *
- * Depends:
- *	ui.core.js
+(function(a){a.widget("ui.droppable",{_init:function(){var c=this.options,b=c.accept;this.isover=0;this.isout=1;this.options.accept=this.options.accept&&a.isFunction(this.options.accept)?this.options.accept:function(e){return e.is(b)};this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight};a.ui.ddmanager.droppables[this.options.scope]=a.ui.ddmanager.droppables[this.options.scope]||[];a.ui.ddmanager.droppables[this.options.scope].push(this);(this.options.addClasses&&this.element.addClass("ui-droppable"))},destroy:function(){var b=a.ui.ddmanager.droppables[this.options.scope];for(var c=0;c<b.length;c++){if(b[c]==this){b.splice(c,1)}}this.element.removeClass("ui-droppable ui-droppable-disabled").removeData("droppable").unbind(".droppable")},_setData:function(b,c){if(b=="accept"){this.options.accept=c&&a.isFunction(c)?c:function(e){return e.is(c)}}else{a.widget.prototype._setData.apply(this,arguments)}},_activate:function(c){var b=a.ui.ddmanager.current;if(this.options.activeClass){this.element.addClass(this.options.activeClass)}(b&&this._trigger("activate",c,this.ui(b)))},_deactivate:function(c){var b=a.ui.ddmanager.current;if(this.options.activeClass){this.element.removeClass(this.options.activeClass)}(b&&this._trigger("deactivate",c,this.ui(b)))},_over:function(c){var b=a.ui.ddmanager.current;if(!b||(b.currentItem||b.element)[0]==this.element[0]){return}if(this.options.accept.call(this.element[0],(b.currentItem||b.element))){if(this.options.hoverClass){this.element.addClass(this.options.hoverClass)}this._trigger("over",c,this.ui(b))}},_out:function(c){var b=a.ui.ddmanager.current;if(!b||(b.currentItem||b.element)[0]==this.element[0]){return}if(this.options.accept.call(this.element[0],(b.currentItem||b.element))){if(this.options.hoverClass){this.element.removeClass(this.options.hoverClass)}this._trigger("out",c,this.ui(b))}},_drop:function(c,d){var b=d||a.ui.ddmanager.current;if(!b||(b.currentItem||b.element)[0]==this.element[0]){return false}var e=false;this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function(){var f=a.data(this,"droppable");if(f.options.greedy&&a.ui.intersect(b,a.extend(f,{offset:f.element.offset()}),f.options.tolerance)){e=true;return false}});if(e){return false}if(this.options.accept.call(this.element[0],(b.currentItem||b.element))){if(this.options.activeClass){this.element.removeClass(this.options.activeClass)}if(this.options.hoverClass){this.element.removeClass(this.options.hoverClass)}this._trigger("drop",c,this.ui(b));return this.element}return false},ui:function(b){return{draggable:(b.currentItem||b.element),helper:b.helper,position:b.position,absolutePosition:b.positionAbs,offset:b.positionAbs}}});a.extend(a.ui.droppable,{version:"1.7.1",eventPrefix:"drop",defaults:{accept:"*",activeClass:false,addClasses:true,greedy:false,hoverClass:false,scope:"default",tolerance:"intersect"}});a.ui.intersect=function(q,j,o){if(!j.offset){return false}var e=(q.positionAbs||q.position.absolute).left,d=e+q.helperProportions.width,n=(q.positionAbs||q.position.absolute).top,m=n+q.helperProportions.height;var g=j.offset.left,c=g+j.proportions.width,p=j.offset.top,k=p+j.proportions.height;switch(o){case"fit":return(g<e&&d<c&&p<n&&m<k);break;case"intersect":return(g<e+(q.helperProportions.width/2)&&d-(q.helperProportions.width/2)<c&&p<n+(q.helperProportions.height/2)&&m-(q.helperProportions.height/2)<k);break;case"pointer":var h=((q.positionAbs||q.position.absolute).left+(q.clickOffset||q.offset.click).left),i=((q.positionAbs||q.position.absolute).top+(q.clickOffset||q.offset.click).top),f=a.ui.isOver(i,h,p,g,j.proportions.height,j.proportions.width);return f;break;case"touch":return((n>=p&&n<=k)||(m>=p&&m<=k)||(n<p&&m>k))&&((e>=g&&e<=c)||(d>=g&&d<=c)||(e<g&&d>c));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<b.length;d++){if(b[d].options.disabled||(e&&!b[d].options.accept.call(b[d].element[0],(e.currentItem||e.element)))){continue}for(var c=0;c<h.length;c++){if(h[c]==b[d].element[0]){b[d].proportions.height=0;continue droppablesLoop}}b[d].visible=b[d].element.css("display")!="none";if(!b[d].visible){continue}b[d].offset=b[d].element.offset();b[d].proportions={width:b[d].element[0].offsetWidth,height:b[d].element[0].offsetHeight};if(f=="mousedown"){b[d]._activate.call(b[d],g)}}},drop:function(b,c){var d=false;a.each(a.ui.ddmanager.droppables[b.options.scope],function(){if(!this.options){return}if(!this.options.disabled&&this.visible&&a.ui.intersect(b,this,this.options.tolerance)){d=this._drop.call(this,c)}if(!this.options.disabled&&this.visible&&this.options.accept.call(this.element[0],(b.currentItem||b.element))){this.isout=1;this.isover=0;this._deactivate.call(this,c)}});return d},drag:function(b,c){if(b.options.refreshPositions){a.ui.ddmanager.prepareOffsets(b,c)}a.each(a.ui.ddmanager.droppables[b.options.scope],function(){if(this.options.disabled||this.greedyChild||!this.visible){return}var e=a.ui.intersect(b,this,this.options.tolerance);var g=!e&&this.isover==1?"isout":(e&&this.isover==0?"isover":null);if(!g){return}var f;if(this.options.greedy){var d=this.element.parents(":data(droppable):eq(0)");if(d.length){f=a.data(d[0],"droppable");f.greedyChild=(g=="isover"?1:0)}}if(f&&g=="isover"){f.isover=0;f.isout=1;f._out.call(f,c)}this[g]=1;this[g=="isout"?"isover":"isout"]=0;this[g=="isover"?"_over":"_out"].call(this,c);if(f&&g=="isout"){f.isout=0;f.isover=1;f._over.call(f,c)}})}}})(jQuery);;/*
+ * jQuery UI Sortable 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/Sortables
+ *
+ * Depends:
+ *	ui.core.js
  */
 (function(a){a.widget("ui.sortable",a.extend({},a.ui.mouse,{_init:function(){var b=this.options;this.containerCache={};this.element.addClass("ui-sortable");this.refresh();this.floating=this.items.length?(/left|right/).test(this.items[0].item.css("float")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var b=this.items.length-1;b>=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<g.scrollSensitivity){this.scrollParent[0].scrollTop=b=this.scrollParent[0].scrollTop+g.scrollSpeed}else{if(f.pageY-this.overflowOffset.top<g.scrollSensitivity){this.scrollParent[0].scrollTop=b=this.scrollParent[0].scrollTop-g.scrollSpeed}}if((this.overflowOffset.left+this.scrollParent[0].offsetWidth)-f.pageX<g.scrollSensitivity){this.scrollParent[0].scrollLeft=b=this.scrollParent[0].scrollLeft+g.scrollSpeed}else{if(f.pageX-this.overflowOffset.left<g.scrollSensitivity){this.scrollParent[0].scrollLeft=b=this.scrollParent[0].scrollLeft-g.scrollSpeed}}}else{if(f.pageY-a(document).scrollTop()<g.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()-g.scrollSpeed)}else{if(a(window).height()-(f.pageY-a(document).scrollTop())<g.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()+g.scrollSpeed)}}if(f.pageX-a(document).scrollLeft()<g.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()-g.scrollSpeed)}else{if(a(window).width()-(f.pageX-a(document).scrollLeft())<g.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()+g.scrollSpeed)}}}if(b!==false&&a.ui.ddmanager&&!g.dropBehaviour){a.ui.ddmanager.prepareOffsets(this,f)}}this.positionAbs=this._convertPositionTo("absolute");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"}for(var d=this.items.length-1;d>=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)<i&&(e+h)>f&&(e+h)<c;if(this.options.tolerance=="pointer"||this.options.forcePointerForContainers||(this.options.tolerance!="pointer"&&this.helperProportions[this.floating?"width":"height"]>m[this.floating?"width":"height"])){return g}else{return(f<e+(this.helperProportions.width/2)&&d-(this.helperProportions.width/2)<c&&n<k+(this.helperProportions.height/2)&&j-(this.helperProportions.height/2)<i)}},_intersectsWithPointer:function(d){var e=a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,d.top,d.height),c=a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,d.left,d.width),g=e&&c,b=this._getDragVerticalDirection(),f=this._getDragHorizontalDirection();if(!g){return false}return this.floating?(((f&&f=="right")||b=="down")?2:1):(b&&(b=="down"?2:1))},_intersectsWithSides:function(e){var c=a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,e.top+(e.height/2),e.height),d=a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,e.left+(e.width/2),e.width),b=this._getDragVerticalDirection(),f=this._getDragHorizontalDirection();if(this.floating&&f){return((f=="right"&&d)||(f=="left"&&!d))}else{return b&&((b=="down"&&c)||(b=="up"&&!c))}},_getDragVerticalDirection:function(){var b=this.positionAbs.top-this.lastPositionAbs.top;return b!=0&&(b>0?"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<this.items.length;c++){for(var b=0;b<d.length;b++){if(d[b]==this.items[c].item[0]){this.items.splice(c,1)}}}},_refreshItems:function(b){this.items=[];this.containers=[this];var h=this.items;var p=this;var f=[[a.isFunction(this.options.items)?this.options.items.call(this.element[0],b,{item:this.currentItem}):a(this.options.items,this.element),this]];var l=this._connectWith();if(l){for(var e=l.length-1;e>=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<n;d++){var o=a(c[d]);o.data("sortable-item",k);h.push({item:o,instance:k,width:0,height:0,left:0,top:0})}}},refreshPositions:function(b){if(this.offsetParent&&this.helper){this.offset.parent=this._getParentOffset()}for(var d=this.items.length-1;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)<h){h=Math.abs(f-e);g=this.items[b]}}if(!g&&!this.options.dropOnEmpty){continue}this.currentContainer=this.containers[c];g?this._rearrange(d,g,null,true):this._rearrange(d,null,this.containers[c].element,true);this._trigger("change",d,this._uiHash());this.containers[c]._trigger("change",d,this._uiHash(this));this.options.placeholder.update(this.currentContainer,this.placeholder)}this.containers[c]._trigger("over",d,this._uiHash(this));this.containers[c].containerCache.over=1}}else{if(this.containers[c].containerCache.over){this.containers[c]._trigger("out",d,this._uiHash(this));this.containers[c].containerCache.over=0}}}},_createHelper:function(c){var d=this.options;var b=a.isFunction(d.helper)?a(d.helper.apply(this.element[0],[c,this.currentItem])):(d.helper=="clone"?this.currentItem.clone():this.currentItem);if(!b.parents("body").length){a(d.appendTo!="parent"?d.appendTo:this.currentItem[0].parentNode)[0].appendChild(b[0])}if(b[0]==this.currentItem[0]){this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}}if(b[0].style.width==""||d.forceHelperSize){b.width(this.currentItem.width())}if(b[0].style.height==""||d.forceHelperSize){b.height(this.currentItem.height())}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.currentItem.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.currentItem.css("marginLeft"),10)||0),top:(parseInt(this.currentItem.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)){var c=a(e.containment)[0];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]}},_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.left<this.containment[0]){d=this.containment[0]+this.offset.click.left}if(e.pageY-this.offset.click.top<this.containment[1]){c=this.containment[1]+this.offset.click.top}if(e.pageX-this.offset.click.left>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.top<this.containment[1]||g-this.offset.click.top>this.containment[3])?g:(!(g-this.offset.click.top<this.containment[1])?g-h.grid[1]:g+h.grid[1])):g;var f=this.originalPageX+Math.round((d-this.originalPageX)/h.grid[0])*h.grid[0];d=this.containment?(!(f-this.offset.click.left<this.containment[0]||f-this.offset.click.left>this.containment[2])?f:(!(f-this.offset.click.left<this.containment[0])?f-h.grid[0]:f+h.grid[0])):f}}return{top:(c-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(i?0:b.scrollTop())))),left:(d-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():i?0:b.scrollLeft())))}},_rearrange:function(g,f,c,e){c?c[0].appendChild(this.placeholder[0]):f.item[0].parentNode.insertBefore(this.placeholder[0],(this.direction=="down"?f.item[0]:f.item[0].nextSibling));this.counter=this.counter?++this.counter:1;var d=this,b=this.counter;window.setTimeout(function(){if(b==d.counter){d.refreshPositions(!e)}},0)},_clear:function(d,e){this.reverting=false;var f=[],b=this;if(!this._noFinalSort&&this.currentItem[0].parentNode){this.placeholder.before(this.currentItem)}this._noFinalSort=null;if(this.helper[0]==this.currentItem[0]){for(var c in this._storedCSS){if(this._storedCSS[c]=="auto"||this._storedCSS[c]=="static"){this._storedCSS[c]=""}}this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else{this.currentItem.show()}if(this.fromOutside&&!e){f.push(function(g){this._trigger("receive",g,this._uiHash(this.fromOutside))})}if((this.fromOutside||this.domPosition.prev!=this.currentItem.prev().not(".ui-sortable-helper")[0]||this.domPosition.parent!=this.currentItem.parent()[0])&&!e){f.push(function(g){this._trigger("update",g,this._uiHash())})}if(!a.ui.contains(this.element[0],this.currentItem[0])){if(!e){f.push(function(g){this._trigger("remove",g,this._uiHash())})}for(var c=this.containers.length-1;c>=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<f.length;c++){f[c].call(this,d)}this._trigger("stop",d,this._uiHash())}return false}if(!e){this._trigger("beforeStop",d,this._uiHash())}this.placeholder[0].parentNode.removeChild(this.placeholder[0]);if(this.helper[0]!=this.currentItem[0]){this.helper.remove()}this.helper=null;if(!e){for(var c=0;c<f.length;c++){f[c].call(this,d)}this._trigger("stop",d,this._uiHash())}this.fromOutside=false;return true},_trigger:function(){if(a.widget.prototype._trigger.apply(this,arguments)===false){this.cancel()}},_uiHash:function(c){var b=c||this;return{helper:b.helper,placeholder:b.placeholder||a([]),position:b.position,absolutePosition:b.positionAbs,offset:b.positionAbs,item:b.currentItem,sender:c?c.element:null}}}));a.extend(a.ui.sortable,{getter:"serialize toArray",version:"1.7.1",eventPrefix:"sort",defaults:{appendTo:"parent",axis:false,cancel:":input,option",connectWith:false,containment:false,cursor:"auto",cursorAt:false,delay:0,distance:1,dropOnEmpty:true,forcePlaceholderSize:false,forceHelperSize:false,grid:false,handle:false,helper:"original",items:"> *",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

Modified: servres/trunk/conifer/static/jquery/js/jquery.js
===================================================================
--- servres/trunk/conifer/static/jquery/js/jquery.js	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/static/jquery/js/jquery.js	2010-01-22 02:48:02 UTC (rev 762)
@@ -536,156 +536,156 @@
 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

Modified: servres/trunk/conifer/static/main.css
===================================================================
--- servres/trunk/conifer/static/main.css	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/static/main.css	2010-01-22 02:48:02 UTC (rev 762)
@@ -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; }
+

Modified: servres/trunk/conifer/static/xslt/test.xsl
===================================================================
--- servres/trunk/conifer/static/xslt/test.xsl	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/static/xslt/test.xsl	2010-01-22 02:48:02 UTC (rev 762)
@@ -1,18 +1,18 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-libxslt needs explicit namespaces
--->
-<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
-xmlns:marc="http://www.loc.gov/MARC21/slim"  version="1.0">
-
-<xsl:template match="text()"/>
-
-<xsl:template match="/marc:record">
-        <xsl:apply-templates/>
-</xsl:template>
-
-<xsl:template match="marc:datafield[@tag='245']">
-<xsl:value-of select="marc:subfield[@code='a']"/>
-</xsl:template>
-
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+libxslt needs explicit namespaces
+-->
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+xmlns:marc="http://www.loc.gov/MARC21/slim"  version="1.0">
+
+<xsl:template match="text()"/>
+
+<xsl:template match="/marc:record">
+        <xsl:apply-templates/>
+</xsl:template>
+
+<xsl:template match="marc:datafield[@tag='245']">
+<xsl:value-of select="marc:subfield[@code='a']"/>
+</xsl:template>
+
 </xsl:stylesheet>
\ No newline at end of file

Modified: servres/trunk/conifer/syrup/urls.py
===================================================================
--- servres/trunk/conifer/syrup/urls.py	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/syrup/urls.py	2010-01-22 02:48:02 UTC (rev 762)
@@ -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<course_id>\d+)/item/(?P<item_id>\d+)/'
-GENERIC_REGEX = r'((?P<obj_id>\d+)/)?(?P<action>.+)?$'
-
-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_option>.*)/$', '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>.*)$', 'instructor_search'),
-    #MARK: propose we kill departments, we have browse
-    (r'^departments/$', 'departments'),
-    (r'^course/(?P<course_id>\d+)/$', 'course_detail'),
-    (r'^instructor/(?P<instructor_id>.*)/$', 'instructor_detail'),
-    (r'^department/(?P<department_id>.*)/$', 'department_detail'),
-    (r'^course/(?P<course_id>\d+)/search/$', 'course_search'),
-    (r'^course/(?P<course_id>\d+)/edit/$', 'edit_course'),
-    (r'^course/(?P<course_id>\d+)/edit/delete/$', 'delete_course'),
-    (r'^course/(?P<course_id>\d+)/edit/permission/$', 'edit_course_permissions'),
-    (r'^course/(?P<course_id>\d+)/feeds/(?P<feed_type>.*)$', 'course_feeds'),
-    (r'^course/(?P<course_id>\d+)/join/$', 'course_join'),
-    (ITEM_PREFIX + r'$', 'item_detail'),
-    (ITEM_PREFIX + r'dl/(?P<filename>.*)$', '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<course_id>\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<term_id>\d+)/$', 'admin_term_edit'),
-#     (r'^admin/terms/(?P<term_id>\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<course_id>\d+)/item/(?P<item_id>\d+)/'
+GENERIC_REGEX = r'((?P<obj_id>\d+)/)?(?P<action>.+)?$'
+
+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_option>.*)/$', '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>.*)$', 'instructor_search'),
+    #MARK: propose we kill departments, we have browse
+    (r'^departments/$', 'departments'),
+    (r'^course/(?P<course_id>\d+)/$', 'course_detail'),
+    (r'^instructor/(?P<instructor_id>.*)/$', 'instructor_detail'),
+    (r'^department/(?P<department_id>.*)/$', 'department_detail'),
+    (r'^course/(?P<course_id>\d+)/search/$', 'course_search'),
+    (r'^course/(?P<course_id>\d+)/edit/$', 'edit_course'),
+    (r'^course/(?P<course_id>\d+)/edit/delete/$', 'delete_course'),
+    (r'^course/(?P<course_id>\d+)/edit/permission/$', 'edit_course_permissions'),
+    (r'^course/(?P<course_id>\d+)/feeds/(?P<feed_type>.*)$', 'course_feeds'),
+    (r'^course/(?P<course_id>\d+)/join/$', 'course_join'),
+    (ITEM_PREFIX + r'$', 'item_detail'),
+    (ITEM_PREFIX + r'dl/(?P<filename>.*)$', '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<course_id>\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<term_id>\d+)/$', 'admin_term_edit'),
+#     (r'^admin/terms/(?P<term_id>\d+)/delete$', 'admin_term_delete'),
+#     (r'^admin/terms/$', 'admin_term'),
+)

Modified: servres/trunk/conifer/syrup/views/general.py
===================================================================
--- servres/trunk/conifer/syrup/views/general.py	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/syrup/views/general.py	2010-01-22 02:48:02 UTC (rev 762)
@@ -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("<?xml version=\"1.0\"?>") + (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)
-
- at 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("<?xml version=\"1.0\"?>") + (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)
+
+ at 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)
+

Modified: servres/trunk/conifer/syrup/views/items.py
===================================================================
--- servres/trunk/conifer/syrup/views/items.py	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/syrup/views/items.py	2010-01-22 02:48:02 UTC (rev 762)
@@ -1,510 +1,510 @@
-from _common import *
-from django.utils.translation import ugettext as _
-
- at 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)
-
- at 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)
-
-
- at 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())
-
- at 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)
-
- at 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())
-        
- at 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')
-    
- at 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'");
-
- at 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)
-
- at 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)
-
-
- at 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
-
- at admin_only                     # fixme, is this the right permission?
-def phys_index(request):
-    return g.render('phys/index.xhtml')
-
- at 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) &mdash; %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 &mdash; %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)
-
-#------------------------------------------------------------
-
- at 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)
-
- at 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')
-
- at 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 _
+
+ at 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)
+
+ at 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)
+
+
+ at 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())
+
+ at 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)
+
+ at 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())
+        
+ at 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')
+    
+ at 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'");
+
+ at 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)
+
+ at 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)
+
+
+ at 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
+
+ at admin_only                     # fixme, is this the right permission?
+def phys_index(request):
+    return g.render('phys/index.xhtml')
+
+ at 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) &mdash; %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 &mdash; %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)
+
+#------------------------------------------------------------
+
+ at 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)
+
+ at 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')
+
+ at 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)
+    
+

Modified: servres/trunk/conifer/templates/departments.xhtml
===================================================================
--- servres/trunk/conifer/templates/departments.xhtml	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/templates/departments.xhtml	2010-01-22 02:48:02 UTC (rev 762)
@@ -1,26 +1,26 @@
-<?python
-title = defined('custom_title') and custom_title or _('Departments')
-?>
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
-      xmlns:py="http://genshi.edgewall.org/">
-<xi:include href="master.xhtml"/>
-<xi:include href="paginate.xhtml"/>
-<head>
-  <title>${title}</title>
-  <script type="text/javascript">
-    <!-- !This ought to be in paginate.xhtml, not here. how to do? -->
-    $(function() { $('.pagetable').tablesorter(); });
-  </script>
-</head>
-<body>
-  <h1>${title}</h1>
-  <tr py:def="pageheader()">
-    <th>Department</th>
-  </tr>
-  <span py:def="pagerow(department)">
-    <td><a href="${ROOT}${department_url(department)}">${department.name}</a></td>
-  </span>
-  ${pagetable(paginator, count, pagerow, pageheader)}
-</body>
-</html>
+<?python
+title = defined('custom_title') and custom_title or _('Departments')
+?>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+<xi:include href="master.xhtml"/>
+<xi:include href="paginate.xhtml"/>
+<head>
+  <title>${title}</title>
+  <script type="text/javascript">
+    <!-- !This ought to be in paginate.xhtml, not here. how to do? -->
+    $(function() { $('.pagetable').tablesorter(); });
+  </script>
+</head>
+<body>
+  <h1>${title}</h1>
+  <tr py:def="pageheader()">
+    <th>Department</th>
+  </tr>
+  <span py:def="pagerow(department)">
+    <td><a href="${ROOT}${department_url(department)}">${department.name}</a></td>
+  </span>
+  ${pagetable(paginator, count, pagerow, pageheader)}
+</body>
+</html>

Modified: servres/trunk/conifer/templates/edit_course.xhtml
===================================================================
--- servres/trunk/conifer/templates/edit_course.xhtml	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/templates/edit_course.xhtml	2010-01-22 02:48:02 UTC (rev 762)
@@ -1,58 +1,58 @@
-<?python
-if instance.id:
-    title = _('Edit course details')
-else:
-    title = _('Create a new course site')
-?>
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
-      xmlns:py="http://genshi.edgewall.org/">
-<xi:include href="master.xhtml"/>
-<xi:include href="components/course.xhtml"/>
-<head>
-  <title>${title}</title>
-  <!--
-  Disabling this title lookup for now, something goes amiss in the browser interaction
-  and the title field is disabled when it shouldn't be - art
-  -->
-  <!--
-  <script type="text/javascript" src="${ROOT}/static/edit_course.js"/>
-  -->
-</head>
-<body>
-  <div py:if="instance.id">${course_banner(instance)}</div>
-  <h1>${title}</h1>
-  <p py:if="instance.id"><a href="permission/">Edit course permissions</a> &bull; <a href="${instance.course_url()}">Return to course page</a></p>
-  <form action="." method="POST">
-    <tr py:def="field_row(field, example=None)">
-      <th>${field.label}</th>
-      <td>
-	<ul py:if="field.errors" class="errorlist">
-	  <li py:for="err in field.errors">${err}</li>
-	</ul>
-	${Markup(field)}
-      </td>
-      <td class="example" py:if="example">e.g., ${example}</td>
-    </tr>
-    <h2>General description</h2>
-    <table class="metadata_table">
-    ${field_row(form.code, example)}
-    ${field_row(form.title)}
-    ${field_row(form.term)}
-    ${field_row(form.department)}
-    <!-- <tr><th>Department</th><td>${Markup(form.department)} ${errorlist(form.department)}</td></tr> -->
-  </table>
-  <p><input type="submit" value="Continue"/> ${go_back_link()}</p>
-  </form>
-  <div class="gap"/>
-  <div py:if="instance.id">
-    <h2>Delete this course</h2>
-    <form action="delete/" method="POST">
-      <p><input type="checkbox" name="confirm_delete" id="confirm_delete"/>
-      <label for="confirm_delete">Yes, I want to delete this course site and all of its contents.</label>
-      </p>
-      <p><input type="submit" value="Delete this course"/> ${go_back_link()}</p>
-    </form>
-  </div>
-</body>
-</html>
+<?python
+if instance.id:
+    title = _('Edit course details')
+else:
+    title = _('Create a new course site')
+?>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+<xi:include href="master.xhtml"/>
+<xi:include href="components/course.xhtml"/>
+<head>
+  <title>${title}</title>
+  <!--
+  Disabling this title lookup for now, something goes amiss in the browser interaction
+  and the title field is disabled when it shouldn't be - art
+  -->
+  <!--
+  <script type="text/javascript" src="${ROOT}/static/edit_course.js"/>
+  -->
+</head>
+<body>
+  <div py:if="instance.id">${course_banner(instance)}</div>
+  <h1>${title}</h1>
+  <p py:if="instance.id"><a href="permission/">Edit course permissions</a> &bull; <a href="${instance.course_url()}">Return to course page</a></p>
+  <form action="." method="POST">
+    <tr py:def="field_row(field, example=None)">
+      <th>${field.label}</th>
+      <td>
+	<ul py:if="field.errors" class="errorlist">
+	  <li py:for="err in field.errors">${err}</li>
+	</ul>
+	${Markup(field)}
+      </td>
+      <td class="example" py:if="example">e.g., ${example}</td>
+    </tr>
+    <h2>General description</h2>
+    <table class="metadata_table">
+    ${field_row(form.code, example)}
+    ${field_row(form.title)}
+    ${field_row(form.term)}
+    ${field_row(form.department)}
+    <!-- <tr><th>Department</th><td>${Markup(form.department)} ${errorlist(form.department)}</td></tr> -->
+  </table>
+  <p><input type="submit" value="Continue"/> ${go_back_link()}</p>
+  </form>
+  <div class="gap"/>
+  <div py:if="instance.id">
+    <h2>Delete this course</h2>
+    <form action="delete/" method="POST">
+      <p><input type="checkbox" name="confirm_delete" id="confirm_delete"/>
+      <label for="confirm_delete">Yes, I want to delete this course site and all of its contents.</label>
+      </p>
+      <p><input type="submit" value="Delete this course"/> ${go_back_link()}</p>
+    </form>
+  </div>
+</body>
+</html>

Modified: servres/trunk/conifer/templates/item/item_add_cat_search.xhtml
===================================================================
--- servres/trunk/conifer/templates/item/item_add_cat_search.xhtml	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/templates/item/item_add_cat_search.xhtml	2010-01-22 02:48:02 UTC (rev 762)
@@ -1,89 +1,89 @@
-<?python
-from django.utils.simplejson import dumps
-from conifer.libsystems.z3950.marcxml import marcxml_dictionary_to_dc as to_dublin
-title = _('Add physical or electronic item, by catalogue search')
-helptext = _('Use keywords or CCL syntax for searching, for example: ti="detroit river" and au="wilgus"')
-dc_keys = ['dc:title', 'dc:creator', 'dc:publisher', 'dc:date']
-?>
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
-      xmlns:py="http://genshi.edgewall.org/">
-<xi:include href="../master.xhtml"/>
-<xi:include href="../paginate.xhtml"/>
-<xi:include href="../components/course.xhtml"/>
-<head>
-  <title>${title}</title>
-  <script type="text/javascript">
-    <!-- !This ought to be in paginate.xhtml, not here. how to do? -->
-    $(function() { $('.pagetable').tablesorter(); });
-  </script>
-  <script py:if="not 'query' in request.GET">		       <!-- !focus on query box if nothing to scroll. -->
-    $(function() { $('#query').focus(); });
-  </script>
-</head>
-<body>
-    ${course_banner(course)}
-    ${nested_title(parent_item)}
-    <h2>${title}</h2>
-    <div class="helptext">
-    ${helptext}
-    </div>
-
-    <form method="GET" action=".">
-      <input type="text" id="query" name="query" value="${query}" 
-	     style="font-size: larger; width: 600px;"/>
-      <input type="submit" value="Search"/>
-	${go_back_link()}
-
-    </form>
-    <div py:def="page_control" py:if="results">
-      <p>
-	${start}&ndash;${min(numhits, start+limit-1)} of ${numhits} results.
-	<span py:if="start-limit&gt;0">
-	  <a href=".?query=${query}&amp;start=${start-limit}&amp;limit=${limit}">Previous ${limit}</a>
-	  &bull;
-	</span>
-	<span py:if="start+limit&lt;numhits">
-	  <a href=".?query=${query}&amp;start=${start+limit}&amp;limit=${limit}">Next ${limit}</a>
-	</span>
-      </p>
-    </div>
-    ${page_control()}
-    <table class="pagetable" py:if="'query' in request.GET">
-      <thead>
-	<tr><th>#</th><th>Title</th><th>Author</th><th>Publisher</th><th>PubDate</th></tr>
-      </thead>
-      <tbody py:for="resultnum, res in enumerate(results)"
-	     py:with="dc=to_dublin(res)">
-	<tr>
-	  <td>${resultnum+start}.</td>
-	  <td>
-	    ${dc.get('dc:title', '???')}
-	    <a href="javascript:$('#full_${resultnum}').toggle(); void(0);">details</a>
-	    <p py:if="res.get('8569')" style="margin: 8px 0; font-size: 90%; color: darkred;">
-	      Electronic resource. <a href="${res.get('856u')}">view</a>
-	    </p>
-	  </td>
-	  <td py:for="k in dc_keys[1:]">${dc.get(k) or '&mdash;'}</td>
-	  <td>
-	    <form action="." method="POST">
-	      <input type="hidden" name="pickitem" value="${dumps(res)}"/>
-	      <input type="submit" value="Pick this item"/>
-	    </form>
-	  </td>
-	</tr>
-	<tr id="full_${resultnum}" style="display: none;">
-	  <td colspan="4" style="padding-left: 36;">
-	    <table class="metadata_table">
-	      <?python allkeys = res.keys(); allkeys.sort(); ?>
-	      <tr py:for="k in allkeys">
-		<th>${k}</th><td>${res[k]}</td>
-	      </tr>
-	    </table>
-	  </td>
-	</tr>
-      </tbody>
-    </table>
-    ${page_control()}
- </body>
-</html>
+<?python
+from django.utils.simplejson import dumps
+from conifer.libsystems.z3950.marcxml import marcxml_dictionary_to_dc as to_dublin
+title = _('Add physical or electronic item, by catalogue search')
+helptext = _('Use keywords or CCL syntax for searching, for example: ti="detroit river" and au="wilgus"')
+dc_keys = ['dc:title', 'dc:creator', 'dc:publisher', 'dc:date']
+?>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+<xi:include href="../master.xhtml"/>
+<xi:include href="../paginate.xhtml"/>
+<xi:include href="../components/course.xhtml"/>
+<head>
+  <title>${title}</title>
+  <script type="text/javascript">
+    <!-- !This ought to be in paginate.xhtml, not here. how to do? -->
+    $(function() { $('.pagetable').tablesorter(); });
+  </script>
+  <script py:if="not 'query' in request.GET">		       <!-- !focus on query box if nothing to scroll. -->
+    $(function() { $('#query').focus(); });
+  </script>
+</head>
+<body>
+    ${course_banner(course)}
+    ${nested_title(parent_item)}
+    <h2>${title}</h2>
+    <div class="helptext">
+    ${helptext}
+    </div>
+
+    <form method="GET" action=".">
+      <input type="text" id="query" name="query" value="${query}" 
+	     style="font-size: larger; width: 600px;"/>
+      <input type="submit" value="Search"/>
+	${go_back_link()}
+
+    </form>
+    <div py:def="page_control" py:if="results">
+      <p>
+	${start}&ndash;${min(numhits, start+limit-1)} of ${numhits} results.
+	<span py:if="start-limit&gt;0">
+	  <a href=".?query=${query}&amp;start=${start-limit}&amp;limit=${limit}">Previous ${limit}</a>
+	  &bull;
+	</span>
+	<span py:if="start+limit&lt;numhits">
+	  <a href=".?query=${query}&amp;start=${start+limit}&amp;limit=${limit}">Next ${limit}</a>
+	</span>
+      </p>
+    </div>
+    ${page_control()}
+    <table class="pagetable" py:if="'query' in request.GET">
+      <thead>
+	<tr><th>#</th><th>Title</th><th>Author</th><th>Publisher</th><th>PubDate</th></tr>
+      </thead>
+      <tbody py:for="resultnum, res in enumerate(results)"
+	     py:with="dc=to_dublin(res)">
+	<tr>
+	  <td>${resultnum+start}.</td>
+	  <td>
+	    ${dc.get('dc:title', '???')}
+	    <a href="javascript:$('#full_${resultnum}').toggle(); void(0);">details</a>
+	    <p py:if="res.get('8569')" style="margin: 8px 0; font-size: 90%; color: darkred;">
+	      Electronic resource. <a href="${res.get('856u')}">view</a>
+	    </p>
+	  </td>
+	  <td py:for="k in dc_keys[1:]">${dc.get(k) or '&mdash;'}</td>
+	  <td>
+	    <form action="." method="POST">
+	      <input type="hidden" name="pickitem" value="${dumps(res)}"/>
+	      <input type="submit" value="Pick this item"/>
+	    </form>
+	  </td>
+	</tr>
+	<tr id="full_${resultnum}" style="display: none;">
+	  <td colspan="4" style="padding-left: 36;">
+	    <table class="metadata_table">
+	      <?python allkeys = res.keys(); allkeys.sort(); ?>
+	      <tr py:for="k in allkeys">
+		<th>${k}</th><td>${res[k]}</td>
+	      </tr>
+	    </table>
+	  </td>
+	</tr>
+      </tbody>
+    </table>
+    ${page_control()}
+ </body>
+</html>

Modified: servres/trunk/conifer/templates/master.xhtml
===================================================================
--- servres/trunk/conifer/templates/master.xhtml	2010-01-19 03:33:20 UTC (rev 761)
+++ servres/trunk/conifer/templates/master.xhtml	2010-01-22 02:48:02 UTC (rev 762)
@@ -1,79 +1,79 @@
-<?python
-app_name = _('Syrup Reserves System')
-search = _('search...')
-import os
-?>
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:py="http://genshi.edgewall.org/"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
-      py:strip="">
-  <py:match path="head" once="True">
-    <head py:attrs="select('@*')"
-	  py:with="t=list(select('title/text()'))">
-      <title>${app_name}<py:if test="t">: ${t}</py:if></title>
-    <link rel="stylesheet" type="text/css" href="${ROOT}/static/main.css"/>
-    <!--
-    using the trunk version of jquery to get around some IE 8 problems
-    -->
-    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery.js"/>
-    <!--
-    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery-1.3.2.min.js"/>
-    -->
-    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery-ui-1.7.1.custom.min.js"/>
-    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery.tablesorter.min.js"/>
-    ${select('*[local-name()!="title"]')}
-    </head>
-  </py:match>
-  <py:match path="body" once="true">
-    <body py:attrs="select('@*')">
-      <div id="outer">
-      <div id="brandheader">
-	<div style="float: right; font-size: x-large; padding: 12px; color: #888;">
-	  ${app_name}
-	</div>
-	<img src="${ROOT}/static/institution-logo.png" style="height: 50px;"/>
-      </div>
-        <!--
-      <div id="header" py:if="user.is_authenticated()">
-        trying to keep the search box consistent for now
-        -->
-      <div id="header">
-        <div id="search">
-            <form method="get" action="${ROOT}/search" 
-                onsubmit="if(q.value.replace(/^\s*/, '').replace(/\s*$/, '') =='') return false;"
-            >
-            <input id="q" name="q" maxlength="100" size="25" type="text" 
-                value="${search}" onblur="if(this.value=='') this.value='${search}';" 
-                onfocus="if(this.value=='${search}') this.value='';"/>
-            </form>
-        </div>
-      <div id="welcome" py:if="user.is_authenticated()">
-	<strong style="padding-right: 18px;">Welcome, ${user.first_name or user.username}!</strong>
-	<a href="${ROOT}/accounts/logout">Log Out</a>
-	&bull; <a href="${ROOT}/prefs/">Preferences</a>
-      </div>
-      <div id="welcome" py:if="not user.is_authenticated()">
-	<strong style="padding-right: 18px;">Welcome!</strong>
-	<a class="loginbutton" href="${ROOT}/accounts/login/">Log In</a>
-	&bull; <a href="${ROOT}/prefs/">Preferences</a>
-      </div>
-    </div>
-      <xi:include py:if="user.is_authenticated()" href="tabbar.xhtml"/>
-      <xi:include py:if="not user.is_authenticated()" href="tabbar_anonymous.xhtml"/>
-      <div id="mainpanel">
-	${select('*|text()')}
-      </div>
-      <div id="footer">
-	<div> 
-    Syrup is a subproject of <a href="http://conifer.mcmaster.ca/">Project Conifer</a> &copy; 2009
-    </div>
-      </div>
-      </div>
-    </body>
-  </py:match>
-
-  <span py:def="go_back_link(url=None, msg=_('or Cancel'))" py:with="url=url or request.META.get('HTTP_REFERER', '../')">
-    <a style="margin-left: 12px;" href="${url}">${msg}</a>
-  </span>
-
-</html>
+<?python
+app_name = _('Syrup Reserves System')
+search = _('search...')
+import os
+?>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      py:strip="">
+  <py:match path="head" once="True">
+    <head py:attrs="select('@*')"
+	  py:with="t=list(select('title/text()'))">
+      <title>${app_name}<py:if test="t">: ${t}</py:if></title>
+    <link rel="stylesheet" type="text/css" href="${ROOT}/static/main.css"/>
+    <!--
+    using the trunk version of jquery to get around some IE 8 problems
+    -->
+    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery.js"/>
+    <!--
+    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery-1.3.2.min.js"/>
+    -->
+    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery-ui-1.7.1.custom.min.js"/>
+    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery.tablesorter.min.js"/>
+    ${select('*[local-name()!="title"]')}
+    </head>
+  </py:match>
+  <py:match path="body" once="true">
+    <body py:attrs="select('@*')">
+      <div id="outer">
+      <div id="brandheader">
+	<div style="float: right; font-size: x-large; padding: 12px; color: #888;">
+	  ${app_name}
+	</div>
+	<img src="${ROOT}/static/institution-logo.png" style="height: 50px;"/>
+      </div>
+        <!--
+      <div id="header" py:if="user.is_authenticated()">
+        trying to keep the search box consistent for now
+        -->
+      <div id="header">
+        <div id="search">
+            <form method="get" action="${ROOT}/search" 
+                onsubmit="if(q.value.replace(/^\s*/, '').replace(/\s*$/, '') =='') return false;"
+            >
+            <input id="q" name="q" maxlength="100" size="25" type="text" 
+                value="${search}" onblur="if(this.value=='') this.value='${search}';" 
+                onfocus="if(this.value=='${search}') this.value='';"/>
+            </form>
+        </div>
+      <div id="welcome" py:if="user.is_authenticated()">
+	<strong style="padding-right: 18px;">Welcome, ${user.first_name or user.username}!</strong>
+	<a href="${ROOT}/accounts/logout">Log Out</a>
+	&bull; <a href="${ROOT}/prefs/">Preferences</a>
+      </div>
+      <div id="welcome" py:if="not user.is_authenticated()">
+	<strong style="padding-right: 18px;">Welcome!</strong>
+	<a class="loginbutton" href="${ROOT}/accounts/login/">Log In</a>
+	&bull; <a href="${ROOT}/prefs/">Preferences</a>
+      </div>
+    </div>
+      <xi:include py:if="user.is_authenticated()" href="tabbar.xhtml"/>
+      <xi:include py:if="not user.is_authenticated()" href="tabbar_anonymous.xhtml"/>
+      <div id="mainpanel">
+	${select('*|text()')}
+      </div>
+      <div id="footer">
+	<div> 
+    Syrup is a subproject of <a href="http://conifer.mcmaster.ca/">Project Conifer</a> &copy; 2009
+    </div>
+      </div>
+      </div>
+    </body>
+  </py:match>
+
+  <span py:def="go_back_link(url=None, msg=_('or Cancel'))" py:with="url=url or request.META.get('HTTP_REFERER', '../')">
+    <a style="margin-left: 12px;" href="${url}">${msg}</a>
+  </span>
+
+</html>



More information about the open-ils-commits mailing list