stripped out pesky Windows carriage returns
authorgfawcett <gfawcett@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Fri, 22 Jan 2010 02:48:02 +0000 (02:48 +0000)
committergfawcett <gfawcett@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Fri, 22 Jan 2010 02:48:02 +0000 (02:48 +0000)
git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@762 6d9bc8c9-1ec2-4278-b937-99fde70a366f

18 files changed:
conifer/README
conifer/custom/course_codes.py
conifer/custom/lib_integration.py
conifer/libsystems/sip/sipclient.py
conifer/libsystems/z3950/pyz3950_search.py
conifer/settings.py
conifer/static/edit_course.js
conifer/static/jquery/js/jquery-ui-1.7.1.custom.min.js
conifer/static/jquery/js/jquery.js
conifer/static/main.css
conifer/static/xslt/test.xsl
conifer/syrup/urls.py
conifer/syrup/views/general.py
conifer/syrup/views/items.py
conifer/templates/departments.xhtml
conifer/templates/edit_course.xhtml
conifer/templates/item/item_add_cat_search.xhtml
conifer/templates/master.xhtml

index b0e94e9..f7656f1 100644 (file)
@@ -1,99 +1,99 @@
-Syrup: A Reserves application\r
-------------------------------\r
-\r
-For more information, see\r
-http://open-ils.org/dokuwiki/doku.php?id=scratchpad:reserves\r
-\r
-or contact\r
-Art Rhyno <artrhyno@uwindsor.ca>\r
-Graham Fawcett <graham.fawcett@gmail.com>\r
-\r
-\r
-State of the application\r
-------------------------------\r
-\r
-Coming along nicely, thank you! With a bit of patience, you ought to\r
-be able to get a basic Syrup system running in no time. Integrating it\r
-with your backend library and other systems will take longer, of\r
-course.\r
-\r
-Required components\r
-------------------------------\r
-\r
-You need Python. Probably Python 2.5, I haven't tested with other\r
-versions. You also need sqlite3 or another Django-compatible\r
-database. Sqlite3 is recommended for kicking the tires, PostgreSQL for\r
-production.\r
-\r
-Third-party Python dependencies:\r
-\r
-  sudo easy_install Django Genshi Babel BabelDjango\r
-\r
-(You'll need 'setuptools' in order to have 'easy_install'.)\r
-\r
-Windows is very similar, see:\r
-\r
-http://groups.google.com/group/syrup-reserves-discuss/web/installing-syrup-in-windows\r
-\r
-Graham has the following versions installed. Not saying you need these\r
-exact ones, just that they are known to work.\r
-\r
-    Django-1.0.1_final-py2.5\r
-    Babel-0.9.4-py2.5\r
-    BabelDjango-0.2.2-py2.5\r
-    Genshi-0.5.1-py2.5\r
-\r
-Getting this thing to run\r
-------------------------------\r
-\r
-This might work:\r
-\r
-* Review settings.py. Maybe you need to edit stuff. Probably not for a\r
-  quick test.\r
-\r
-* ./manage.py syncdb\r
-\r
-Note: don't use the "./" syntax in windows for the commands, e.g.:\r
-\r
-C:\src\syrup\trunk\conifer>manage.py syncdb\r
-\r
-* During syncdb, create yourself a superuser account.\r
-\r
-* ./pybabel-extract  (currently, this is optional)\r
-\r
-* ./manage.py runserver\r
-\r
-* visit http://localhost:8000/ and log in.\r
-\r
-* create at least one Term and one Department under Admin Options.\r
-\r
-* make yourself a course.\r
-\r
-* click on all of the links and see what they do.\r
-\r
-\r
-Contents [out of date -- Ed.]\r
-------------------------------\r
-\r
-syrup/                 -- the reserves app\r
-middleware/            -- middleware component to integrate Genshi\r
-locale/                        -- the gettext files\r
-templates/             -- the Genshi templates\r
-static/                        -- static JS, CSS, image files\r
-doc/                   -- documentation on the app\r
-\r
-local_settings.py.in    -- a template for local_settings.py\r
-genshi_support.py      -- Genshi template integration\r
-pybabel-extract                -- a "make all" for the i18n files\r
-babel.cfg              -- Babel (i18n) configuration file\r
-\r
-The rest is straightforward Django stuff.\r
-\r
-\r
-Customization\r
-------------------------------\r
-\r
-The 'custom' directory contains (or should contain!)  all of the bits\r
-that you really need to customize for your institution. More\r
-documentation is needed here, but the source code is mostly\r
+Syrup: A Reserves application
+------------------------------
+
+For more information, see
+http://open-ils.org/dokuwiki/doku.php?id=scratchpad:reserves
+
+or contact
+Art Rhyno <artrhyno@uwindsor.ca>
+Graham Fawcett <graham.fawcett@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
index 76aa334..7482ad0 100644 (file)
-# Validation and lookup of course codes.\r
-\r
-# This modules specifies an "course-code interface" and a null\r
-# implementation of that interface. If your local system has rules for\r
-# valid course codes, and a mechanism for looking up details of these\r
-# codes, you can implement the interface according to your local\r
-# rules.\r
-\r
-\r
-# ------------------------------------------------------------\r
-# Overview and definitions\r
-\r
-# A course code identifies a specific course offering. Course codes\r
-# map 1:N onto formal course titles: by looking up a code, we can\r
-# derive a formal title (in theory, though it may not be possible for\r
-# external reasons).\r
-\r
-# A course code is insufficient to specify a class list: we need a\r
-# course section for that. A section ties a course code and term to an\r
-# instructor(s) and a list of students.\r
-\r
-# Course codes may have cross-listings, i.e., other codes which refer\r
-# to the same course, but which appear under a different department\r
-# for various academic purposes. In our system, we make no attempt to\r
-# subordinate cross-listings to a "primary" course code.\r
-\r
-\r
-#------------------------------------------------------------\r
-# Notes on the interface\r
-#\r
-# The `course_code_is_valid` function will be used ONLY if\r
-# course_code_list() returns None (it is a null implementation). If a\r
-# course-list is available, the system will use a membership test for\r
-# course-code validity.\r
-#\r
-# `course_code_lookup_title` will be used ONLY if `course_code_list`\r
-# is implemented.\r
-#\r
-#\r
-# "types" of the interface members\r
-#\r
-# course_code_is_valid       (string) --> boolean.\r
-# course_code_example        : a string constant.\r
-# course_code_list           () --> list of strings\r
-# course_code_lookup_title   (string) --> string, or None.\r
-# course_code_cross_listings (string) --> list of strings\r
-#\r
-# For each member, you MUST provide either a valid implementation, or\r
-# set the member to None. See the null implementation below.\r
-\r
-#------------------------------------------------------------\r
-# Implementations\r
-\r
-# ------------------------------------------------------------ \r
-# Here is a 'null implementation' of the course-code interface. No\r
-# validation is done, nor are lookups.\r
-#\r
-#    course_code_is_valid       = None  # anything is OK;\r
-#    course_code_example        = None  # no examples;\r
-#    course_code_lookup_title   = None  # no codes to list;\r
-#    course_code_cross_listings = None  # no cross lists.\r
-\r
-# ------------------------------------------------------------\r
-# This one specifies a valid course-code format using a regular\r
-# expression, and offers some example codes, but does not have a\r
-# lookup system.\r
-#\r
-#    import re\r
-#\r
-#    def course_code_is_valid(course_code):\r
-#        pattern = re.compile(r'^\d{2}-\d{3}$')\r
-#        return bool(pattern.match(course_code))\r
-#\r
-#    course_code_example        = '55-203; 99-105'\r
-#\r
-#    course_code_list           = None\r
-#    course_code_lookup_title   = None\r
-#    course_code_cross_listings = None\r
-\r
-\r
-\r
-# ------------------------------------------------------------\r
-# This is a complete implementation, based on a hard-coded list of\r
-# course codes and titles, and two cross-listed course codes.\r
-#\r
-#    _codes = [('ENG100', 'Introduction to English'),\r
-#              ('ART108', 'English: An Introduction'),\r
-#              ('FRE238', 'Modern French Literature'),\r
-#              ('WEB203', 'Advanced Web Design'),]\r
-#\r
-#    _crosslists = set(['ENG100', 'ART108'])\r
-#\r
-#    course_code_is_valid = None\r
-#    course_code_example = 'ENG100; FRE238'\r
-#\r
-#    def course_code_list():\r
-#        return [a for (a,b) in _codes]\r
-#\r
-#    def course_code_lookup_title(course_code):\r
-#        return dict(_codes).get(course_code)\r
-#\r
-#    def course_code_cross_listings(course_code):\r
-#        if course_code in _crosslists:\r
-#            return list(_crosslists - set([course_code]))\r
-\r
-\r
-# ------------------------------------------------------------\r
-# Provide your own implementation below.\r
-\r
-\r
-#_codes = [('ENG100', 'Introduction to English'),\r
-#          ('ART108', 'English: An Introduction'),\r
-#          ('FRE238', 'Modern French Literature'),\r
-#          ('LIB201', 'Intro to Library Science'),\r
-#          ('WEB203', 'Advanced Web Design'),]\r
-\r
-_codes = [('ART99-100', 'Art History'),\r
-          ('BIOL55-350', 'Molecular Cell Biology'),\r
-          ('CRIM48-567', 'Current Issues in Criminology'),\r
-          ('ENGL26-280', 'Contemporary Literary Theory'),\r
-          ('ENGL26-420', 'Word and Image: The Contemporary Graphic Novel'),\r
-          ('SOCWK47-457', 'Advanced Social Work Research'),]\r
-\r
-_crosslists = set(['ENGL26-280', 'ENGL26-420'])\r
-\r
-\r
-course_code_is_valid = None\r
-\r
-course_code_example = 'BIOL55-350; SOCWK47-457'\r
-\r
-def course_code_list():\r
-    return [a for (a,b) in _codes]\r
-\r
-def course_code_lookup_title(course_code):\r
-    return dict(_codes).get(course_code)\r
-\r
-def course_code_cross_listings(course_code):\r
-    if course_code in _crosslists:\r
-        return list(_crosslists - set([course_code]))\r
-\r
+# 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]))
+
index b1061af..ebe08bf 100644 (file)
@@ -1,98 +1,98 @@
-# Our integration-point with back-end library systems.\r
-\r
-# This is a work in progress. I'm trying to separate out the actual\r
-# protocol handlers (in libsystems) from the configuration decicions\r
-# (in settings.py), and use this as sort of a merge-point between\r
-# those two decisions. \r
-\r
-# TODO: write some documentation about the lib_integration interface.\r
-\r
-# Our example configuration: \r
-# Z39.50 for catalogue search, \r
-# SIP for patron and item_info, and for item checkout and checkin,\r
-# OpenSRF for extended item info.\r
-\r
-# define a @caching decorator to exploit the Django cache. Fixme, move\r
-# this somewhere else.\r
-from django.core.cache import cache\r
-import cPickle \r
-def caching(prefix, timeout=60):\r
-    def g(func):\r
-        def f(*args):\r
-            # wtf! Django encodes string-values as\r
-            # unicode-strings. That's bad, like stupid-bad! I'm\r
-            # putting explicit utf8-conversions here to make debugging\r
-            # easier if this code dies.\r
-            key = ','.join([prefix] + map(str, args))\r
-            v = cache.get(key)\r
-            if v:\r
-                return cPickle.loads(v.encode('utf-8'))\r
-            else:\r
-                v = func(*args)\r
-                if v:\r
-                    cache.set(key, unicode(cPickle.dumps(v), 'utf-8'), timeout)\r
-                    return v\r
-        return f\r
-    return g\r
-\r
-\r
-from django.conf import settings\r
-\r
-from conifer.libsystems.evergreen.support import initialize\r
-EG_BASE = 'http://%s/' % settings.EVERGREEN_GATEWAY_SERVER\r
-try:\r
-    initialize(EG_BASE)\r
-except:\r
-    import warnings\r
-    warnings.warn('Evergreen inaccessible! Integration will suck eggs!')\r
-\r
-from conifer.libsystems.evergreen import item_status as I\r
-from conifer.libsystems.sip.sipclient import SIP\r
-#from conifer.libsystems.z3950 import yaz_search\r
-from conifer.libsystems.z3950 import pyz3950_search\r
-from conifer.libsystems.z3950.marcxml import marcxml_to_dictionary\r
-\r
-\r
-@caching('patroninfo', timeout=300)\r
-@SIP\r
-def patron_info(conn, barcode):\r
-    return conn.patron_info(barcode)\r
-\r
-@caching('itemstatus', timeout=300)\r
-@SIP\r
-def item_status(conn, barcode):\r
-    return conn.item_info(barcode)\r
-\r
-@SIP\r
-def checkout(conn, patron_barcode, item_barcode):\r
-    return conn.checkout(patron_barcode, item_barcode, '')\r
-\r
-@SIP\r
-def checkin(conn, item_barcode):\r
-    return conn.checkin(item_barcode, institution='', location='')\r
-\r
-\r
-@caching('bcbi', timeout=3600)\r
-def barcode_to_bib_id(barcode):\r
-    return I.barcode_to_bib_id(barcode)\r
-\r
-@caching('bccp', timeout=3600)\r
-def barcode_to_copy(barcode):\r
-    return I.barcode_to_copy(barcode)\r
-\r
-@caching('bimx', timeout=3600)\r
-def bib_id_to_marcxml(bib_id):\r
-    return I.bib_id_to_marcxml(bib_id)\r
-\r
-\r
-def cat_search(query, start=1, limit=10):\r
-    # this is a total hack for conifer. If the query is a Conifer\r
-    # title-detail URL, then return just that one item.\r
-    if query.startswith(EG_BASE):\r
-        results = marcxml_to_dictionary(I.url_to_marcxml(query), multiples=True)\r
-        numhits = len(results)\r
-    else:\r
-        cat_host, cat_port, cat_db = settings.Z3950_CONFIG\r
-        results, numhits = pyz3950_search.search(cat_host, cat_port, cat_db, query, start, limit)\r
-        #results, numhits = yaz_search.search(cat_host, cat_db, query, start, limit)\r
-    return results, numhits\r
+# Our integration-point with back-end library systems.
+
+# This is a work in progress. I'm trying to separate out the actual
+# protocol handlers (in libsystems) from the configuration decicions
+# (in settings.py), and use this as sort of a merge-point between
+# those two decisions. 
+
+# TODO: write some documentation about the lib_integration interface.
+
+# Our example configuration: 
+# Z39.50 for catalogue search, 
+# SIP for patron and item_info, and for item checkout and checkin,
+# OpenSRF for extended item info.
+
+# define a @caching decorator to exploit the Django cache. Fixme, move
+# this somewhere else.
+from django.core.cache import cache
+import cPickle 
+def caching(prefix, timeout=60):
+    def g(func):
+        def f(*args):
+            # wtf! Django encodes string-values as
+            # unicode-strings. That's bad, like stupid-bad! I'm
+            # putting explicit utf8-conversions here to make debugging
+            # easier if this code dies.
+            key = ','.join([prefix] + map(str, args))
+            v = cache.get(key)
+            if v:
+                return cPickle.loads(v.encode('utf-8'))
+            else:
+                v = func(*args)
+                if v:
+                    cache.set(key, unicode(cPickle.dumps(v), 'utf-8'), timeout)
+                    return v
+        return f
+    return g
+
+
+from django.conf import settings
+
+from conifer.libsystems.evergreen.support import initialize
+EG_BASE = 'http://%s/' % settings.EVERGREEN_GATEWAY_SERVER
+try:
+    initialize(EG_BASE)
+except:
+    import warnings
+    warnings.warn('Evergreen inaccessible! Integration will suck eggs!')
+
+from conifer.libsystems.evergreen import item_status as I
+from conifer.libsystems.sip.sipclient import SIP
+#from conifer.libsystems.z3950 import yaz_search
+from conifer.libsystems.z3950 import pyz3950_search
+from conifer.libsystems.z3950.marcxml import marcxml_to_dictionary
+
+
+@caching('patroninfo', timeout=300)
+@SIP
+def patron_info(conn, barcode):
+    return conn.patron_info(barcode)
+
+@caching('itemstatus', timeout=300)
+@SIP
+def item_status(conn, barcode):
+    return conn.item_info(barcode)
+
+@SIP
+def checkout(conn, patron_barcode, item_barcode):
+    return conn.checkout(patron_barcode, item_barcode, '')
+
+@SIP
+def checkin(conn, item_barcode):
+    return conn.checkin(item_barcode, institution='', location='')
+
+
+@caching('bcbi', timeout=3600)
+def barcode_to_bib_id(barcode):
+    return I.barcode_to_bib_id(barcode)
+
+@caching('bccp', timeout=3600)
+def barcode_to_copy(barcode):
+    return I.barcode_to_copy(barcode)
+
+@caching('bimx', timeout=3600)
+def bib_id_to_marcxml(bib_id):
+    return I.bib_id_to_marcxml(bib_id)
+
+
+def cat_search(query, start=1, limit=10):
+    # this is a total hack for conifer. If the query is a Conifer
+    # title-detail URL, then return just that one item.
+    if query.startswith(EG_BASE):
+        results = marcxml_to_dictionary(I.url_to_marcxml(query), multiples=True)
+        numhits = len(results)
+    else:
+        cat_host, cat_port, cat_db = settings.Z3950_CONFIG
+        results, numhits = pyz3950_search.search(cat_host, cat_port, cat_db, query, start, limit)
+        #results, numhits = yaz_search.search(cat_host, cat_db, query, start, limit)
+    return results, numhits
index 4bce1f8..0b21960 100644 (file)
-# Small portions are borrowed from David Fiander's acstest.py, in the\r
-# openncip project. David's license is below:\r
-\r
-# Copyright (C) 2006-2008  Georgia Public Library Service\r
-# \r
-# Author: David J. Fiander\r
-# \r
-# This program is free software; you can redistribute it and/or\r
-# modify it under the terms of version 2 of the GNU General Public\r
-# License as published by the Free Software Foundation.\r
-# \r
-# This program is distributed in the hope that it will be useful,\r
-# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
-# GNU General Public License for more details.\r
-# \r
-# You should have received a copy of the GNU General Public\r
-# License along with this program; if not, write to the Free\r
-# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,\r
-# MA 02111-1307 USA\r
-\r
-\r
-from sipconstants import *\r
-import socket\r
-import sys\r
-from datetime import datetime\r
-import re\r
-\r
-DEBUG = True\r
-\r
-# ------------------------------------------------------------\r
-# helper functions\r
-\r
-def split_n(n):\r
-    """Return a function that splits a string into two parts at index N."""\r
-    return lambda s: (s[:n], s[n:])\r
-\r
-split2 = split_n(2)\r
-\r
-\r
-\r
-\r
-# ------------------------------------------------------------\r
-# Messages\r
-\r
-# First we build up a little language for defining SIP messages, so\r
-# that we can define the protocol in a declarative style.\r
-\r
-\r
-class basefield(object): \r
-\r
-    def encode(self, dct):\r
-        """Take a dict, and return the wire representation of this field."""\r
-        raise NotImplementedError, repr(self)\r
-\r
-    def decode(self, bytes):\r
-        """\r
-        Take a wire representation and return a pair (V,R) where V is\r
-        the translated value of the current field, and R is the\r
-        remaining bytes after the field has been read. If this is an\r
-        optional field, then decode should return None for V, and\r
-        return the input bytes for R.\r
-        """\r
-        raise NotImplementedError, repr(self)\r
-\r
-\r
-class field(basefield):\r
-\r
-    def __init__(self, name, code, width=None):\r
-        self.name = name \r
-        self.code = code\r
-        self.width = None       # don't use this yet.\r
-\r
-    def encode(self, dct):\r
-        return '%s%s|' % (self.code, dct.get(self.name, ''))\r
-\r
-    def decode(self, bytes):\r
-        bcode, rest = split2(bytes)\r
-        if bcode != self.code:\r
-            raise 'BadDecode', \\r
-                'Wrong field! Expected %r (%s) got %r (%s), in %r.' % (\r
-                    self.code, lookup_constant(self.code),\r
-                    bcode, lookup_constant(bcode),\r
-                    bytes)\r
-        data, rest = rest.split('|', 1)\r
-        return data, rest\r
-\r
-\r
-class optfield(field):          # an optional field\r
-\r
-    def decode(self, bytes):\r
-        tmp = bytes + '  '\r
-        bcode, rest = split2(tmp)\r
-        if bcode == self.code:\r
-            return field.decode(self, bytes)\r
-        else:\r
-            return None, bytes\r
-\r
-        \r
-class charfield(basefield):\r
-\r
-    def __init__(self, name, width=None, default=None):\r
-        self.name = name\r
-        self.dflt = str(default)\r
-        self.width = width or len(self.dflt) # give at least one\r
-        self.pad = ' ' * self.width\r
-\r
-        self.decode = split_n(self.width)\r
-\r
-    def encode(self, dct):\r
-        v = dct.get(self.name, self.dflt)\r
-        assert v is not None\r
-        return ('%s%s' % (self.pad, v))[-self.width:]\r
-\r
-\r
-class yn(basefield):\r
-    def __init__(self, name):\r
-        self.name = name\r
-\r
-    def encode(self, dct):\r
-        return 'NY'[bool(dct.get(self.name))]\r
-\r
-    def decode(self, bytes):\r
-        return (bytes[0] == 'Y'), bytes[1:]\r
-\r
-\r
-class localtime(charfield):\r
-    def __init__(self, name):\r
-        self.name = name\r
-        self.width = 18\r
-\r
-    def encode(self, dct):\r
-        return datetime.now().strftime('%Y%m%d    %H%M%S')\r
-\r
-    def decode(self, bytes):\r
-        return split_n(self.width)(bytes)\r
-\r
-RAW = -55\r
-class raw(basefield):\r
-    name = 'raw'\r
-    # for debugging.\r
-    def decode(self, bytes):\r
-        return bytes, '\r'\r
-\r
-# We define a protocol Message as a list of fields. For now,\r
-# message(A, B, C) is equivalent to the tuple (A,B,C).\r
-\r
-message = lambda *args: args\r
-\r
-# Encoding a message on to the wire. Args is a dict of field-values.\r
-\r
-def encode_msg(msg, args):\r
-    out = []\r
-    add = out.append\r
-    for thing in msg:\r
-        if isinstance(thing, basefield):\r
-            add(thing.encode(args))\r
-        else:\r
-            add(str(thing))\r
-    return ''.join(out)\r
-\r
-# Decoding from the wire:\r
-\r
-def decode_msg(msg, bytes):\r
-    out = {}\r
-    add = out.__setitem__\r
-    rest = bytes\r
-    \r
-    # Proper 'fields' have variable position in the tail of the\r
-    # message. So we treat them differently.\r
-    varposn = set([p for p in msg if isinstance(p, field)])\r
-    varlookup = dict((x.code, x) for x in varposn)\r
-    fixedposn = [p for p in msg if not p in varposn]\r
-    \r
-    for part in fixedposn:\r
-        if isinstance(part, basefield):\r
-            good, rest = part.decode(rest)\r
-            if good is not None:\r
-                add(part.name, good)\r
-        else:\r
-            v = str(part)\r
-            good, rest = rest[:len(v)], rest[len(v):]\r
-            assert v == good\r
-        if DEBUG: print '%s == %r\n==== %r' % (getattr(part, 'name',''), good, rest)\r
-\r
-    # Now we take what's left, chunk it, and try to resolve each one\r
-    # against a variable-position field.\r
-    segments = re.findall(r'(.*?\|)', rest)\r
-    \r
-    if DEBUG: print segments\r
-\r
-    for segment in segments:\r
-        fld = varlookup.get(segment[:2])\r
-        if fld:\r
-            good, rest = fld.decode(segment)\r
-            add(fld.name, good)\r
-            varposn.remove(fld)\r
-        else:\r
-            raise 'FieldNotProcessed: %s, %s' % (segment, lookup_constant(segment[:2]))\r
-\r
-    # Let's make sure that any "required" fields were not missing.\r
-    notpresent = set(f for f in varposn if not isinstance(f, optfield))\r
-    if notpresent:\r
-        for f in notpresent:\r
-            print 'MISSING: %-12s %s %s' % (f.name, f.code, lookup_constant(f.code))\r
-        raise 'MandatoryFieldsNotPresent'\r
-\r
-    return out\r
-\r
-# The SIP checksum. Borrowed from djfiander.        \r
-\r
-def checksum(msg):\r
-    return '%04X' % ((0 - sum(map(ord, msg))) & 0xFFFF)\r
-\r
-\r
-#------------------------------------------------------------\r
-# SIP Message Definitions\r
-\r
-# some common fields\r
-\r
-\r
-fld_localtime     = localtime('localtime')\r
-fld_INST_ID       = field('inst', FID_INST_ID)\r
-fld_ITEM_ID       = field('item', FID_ITEM_ID)\r
-fld_PATRON_ID     = field('patron', FID_PATRON_ID)\r
-ofld_TERMINAL_PWD = optfield('termpwd', FID_TERMINAL_PWD)\r
-fld_proto_version = charfield('version', default='2.00')\r
-ofld_print_line    = optfield('print_line', FID_PRINT_LINE)\r
-ofld_screen_msg    = optfield('screenmsg', FID_SCREEN_MSG)\r
-\r
-MESSAGES = {\r
-    LOGIN : message(\r
-            LOGIN, \r
-            '00',\r
-            field('uid', FID_LOGIN_UID),\r
-            field('pwd', FID_LOGIN_PWD),\r
-            field('locn', FID_LOCATION_CODE)),\r
-\r
-    LOGIN_RESP : message(\r
-            LOGIN_RESP, \r
-            charfield('ok', width=1)),\r
-\r
-    SC_STATUS : message(\r
-            SC_STATUS, \r
-            charfield('online', default='1'),\r
-            charfield('width', default='040'),\r
-            fld_proto_version),\r
-\r
-    ACS_STATUS : message(\r
-            ACS_STATUS,\r
-            yn('online'),\r
-            yn('checkin_OK'),\r
-            yn('checkout_OK'),\r
-            yn('renewal_OK'),\r
-            yn('status_update_OK'),\r
-            yn('offline_OK'),\r
-            charfield('timeout', default='01'),\r
-            charfield('retries', default='9999'),\r
-            fld_localtime,\r
-            charfield('protocol', default='2.00'),\r
-            fld_INST_ID,\r
-            optfield('patron_id', FID_PATRON_ID),\r
-            optfield('item_id', FID_ITEM_ID),\r
-            optfield('terminal_pwd', FID_TERMINAL_PWD),\r
-            \r
-            optfield('instname', FID_LIBRARY_NAME),\r
-            field('supported', FID_SUPPORTED_MSGS),\r
-            optfield('ttylocn', FID_TERMINAL_LOCN),\r
-            ofld_screen_msg,\r
-            ofld_print_line),\r
-    PATRON_INFO : message(\r
-            PATRON_INFO,\r
-            charfield('lang', width=3, default=1),\r
-            fld_localtime,\r
-            charfield('holditemsreq', default='Y         '),\r
-            fld_INST_ID,\r
-            fld_PATRON_ID,\r
-            ofld_TERMINAL_PWD,\r
-            optfield('patronpwd', FID_PATRON_PWD),\r
-            optfield('startitem', FID_START_ITEM, width=5),\r
-            optfield('enditem', FID_END_ITEM, width=5)),\r
-            \r
-    PATRON_INFO_RESP : message(\r
-            PATRON_INFO_RESP,\r
-            charfield('hmmm', width=14),\r
-            charfield('lang', width=3, default=1),\r
-            fld_localtime,\r
-            charfield('onhold', width=4),\r
-            charfield('overdue', width=4),\r
-            charfield('charged', width=4),\r
-            charfield('fine', width=4),\r
-            charfield('recall', width=4),\r
-            charfield('unavail_holds', width=4),\r
-            fld_INST_ID,\r
-            ofld_screen_msg,\r
-            ofld_print_line,\r
-            optfield('instname', FID_LIBRARY_NAME),\r
-            fld_PATRON_ID,\r
-            field('personal', FID_PERSONAL_NAME),\r
-\r
-            optfield('hold_limit', FID_HOLD_ITEMS_LMT, width=4),\r
-            optfield('overdue_limit', FID_OVERDUE_ITEMS_LMT, width=4),\r
-            optfield('charged_limit', FID_OVERDUE_ITEMS_LMT, width=4),\r
-\r
-            optfield('hold_items', FID_HOLD_ITEMS),\r
-            optfield('valid_patron_pwd', FID_VALID_PATRON_PWD),\r
-            \r
-            optfield('valid_patron', FID_VALID_PATRON),\r
-            optfield('currency', FID_CURRENCY),\r
-            optfield('fee_amt', FID_FEE_AMT),\r
-            optfield('fee_limit', FID_FEE_LMT),\r
-            optfield('home_addr', FID_HOME_ADDR),\r
-            optfield('email', FID_EMAIL),\r
-            optfield('home_phone', FID_HOME_PHONE),\r
-            optfield('patron_birthdate', FID_PATRON_BIRTHDATE),\r
-            optfield('patron_class', FID_PATRON_CLASS),\r
-            optfield('inet_profile', FID_INET_PROFILE),\r
-            optfield('home_library', FID_HOME_LIBRARY)),\r
-\r
-    END_PATRON_SESSION : message(\r
-            END_PATRON_SESSION,\r
-            fld_localtime,\r
-            field('inst', FID_INST_ID),\r
-            field('patron', FID_PATRON_ID)),\r
-\r
-    END_SESSION_RESP : message(\r
-            END_SESSION_RESP,\r
-            yn('session_ended'),\r
-            fld_localtime,\r
-            fld_INST_ID,\r
-            fld_PATRON_ID,\r
-            ofld_print_line,\r
-            ofld_screen_msg),\r
-\r
-    CHECKOUT: message(\r
-        CHECKOUT,\r
-        yn('renewals_OK'),\r
-        yn('no_block'),\r
-        fld_localtime,\r
-        fld_localtime,\r
-        field('inst', FID_INST_ID),\r
-        field('patron', FID_PATRON_ID),\r
-        field('item', FID_ITEM_ID),\r
-        ),\r
-\r
-    CHECKOUT_RESP: message(\r
-        CHECKOUT_RESP,\r
-        charfield('ok', width=1),\r
-        yn('is_renewal'),\r
-        yn('is_magnetic'),\r
-        yn('desensitize'),\r
-        fld_localtime,\r
-        field('inst', FID_INST_ID),\r
-        field('patron', FID_PATRON_ID),\r
-        field('item', FID_ITEM_ID),\r
-        field('due', FID_DUE_DATE),\r
-        field('title', FID_TITLE_ID),\r
-        optfield('media_type_code', FID_MEDIA_TYPE),\r
-        optfield('is_valid_patron', FID_VALID_PATRON),\r
-        ofld_print_line,\r
-        ofld_screen_msg),\r
-\r
-    CHECKIN: message(\r
-        CHECKIN,\r
-        yn('is_retry'),\r
-        fld_localtime,\r
-        fld_localtime,\r
-        field('item', FID_ITEM_ID),\r
-        field('location', FID_CURRENT_LOCN),\r
-        field('inst', FID_INST_ID),\r
-        ofld_TERMINAL_PWD,\r
-        ),\r
-\r
-    CHECKIN_RESP: message(\r
-        CHECKIN_RESP,\r
-        charfield('ok', width=1),\r
-        yn('resensitize'),\r
-        yn('is_magnetic'),\r
-        yn('alert'),\r
-        fld_localtime,\r
-        fld_INST_ID,\r
-        optfield('patron', FID_PATRON_ID),\r
-        field('item', FID_ITEM_ID),\r
-        field('title', FID_TITLE_ID),\r
-        optfield('media_type_code', FID_MEDIA_TYPE),\r
-        optfield('perm_locn', FID_PERM_LOCN),\r
-        optfield('due', FID_DUE_DATE),\r
-        ofld_print_line,\r
-        ofld_screen_msg,\r
-        ),\r
-#         yn('is_retry'),\r
-#         fld_localtime,\r
-#         fld_localtime,\r
-#         field('item', FID_ITEM_ID),\r
-#         field('location', FID_CURRENT_LOCN),\r
-#         ofld_TERMINAL_PWD,\r
-#        ),\r
-\r
-    ITEM_INFORMATION : message(\r
-            ITEM_INFORMATION,\r
-            fld_localtime,\r
-            fld_INST_ID,\r
-            fld_ITEM_ID,\r
-            ofld_TERMINAL_PWD),\r
-\r
-    ITEM_INFO_RESP : message(\r
-            ITEM_INFO_RESP,\r
-            charfield('circstat', width=2),\r
-            charfield('security', width=2),\r
-            charfield('feetype', width=2),\r
-            fld_localtime,\r
-            fld_ITEM_ID,\r
-            field('title', FID_TITLE_ID),\r
-            optfield('mediatype', FID_MEDIA_TYPE),\r
-            optfield('perm_locn', FID_PERM_LOCN),\r
-            optfield('current_locn', FID_CURRENT_LOCN),\r
-            optfield('item_props', FID_ITEM_PROPS),\r
-            optfield('currency', FID_CURRENCY),\r
-            optfield('fee', FID_FEE_AMT),\r
-            optfield('owner', FID_OWNER),\r
-            optfield('hold_queue_len', FID_HOLD_QUEUE_LEN),\r
-            optfield('due_date', FID_DUE_DATE),\r
-\r
-            optfield('recall_date', FID_RECALL_DATE),\r
-            optfield('hold_pickup_date', FID_HOLD_PICKUP_DATE),\r
-            ofld_screen_msg,\r
-            ofld_print_line),\r
-            \r
-    RAW : message(raw()),\r
-}\r
-\r
-\r
-class SipClient(object):\r
-    def __init__(self, host, port, error_detect=False):\r
-        self.hostport = (host, port)\r
-        self.error_detect = error_detect\r
-        self.connect()\r
-\r
-    def connect(self):\r
-        so = socket.socket()\r
-        so.connect(self.hostport)\r
-        self.socket = so\r
-        self.seqno = self.error_detect and 1 or 0\r
-\r
-    def close(self):\r
-        # fixme, do SIP close first.\r
-        self.socket.close()\r
-\r
-    def send(self, outmsg, inmsg, args=None):\r
-        msg_template = MESSAGES[outmsg]\r
-        resp_template = MESSAGES[inmsg]\r
-        msg = encode_msg(msg_template, args or {})\r
-        if self.error_detect:\r
-            # add the checksum\r
-            msg += 'AY%dAZ' % (self.seqno % 10)\r
-            self.seqno += 1\r
-            msg += checksum(msg)\r
-        msg += '\r'\r
-        if DEBUG: print '>>> %r' % msg\r
-        self.socket.send(msg)\r
-        resp = self.socket.recv(1000)\r
-        if DEBUG: print '<<< %r' % resp\r
-        return decode_msg(resp_template, resp)\r
-        \r
-\r
-    # --------------------------------------------------\r
-    # Common protocol methods\r
-\r
-    def login(self, uid, pwd, locn):\r
-        msg = self.send(LOGIN, LOGIN_RESP, \r
-                        dict(uid=uid, pwd=pwd, locn=locn))\r
-        return msg.get('ok') == '1'\r
-\r
-    def status(self):\r
-        return self.send(SC_STATUS, ACS_STATUS)\r
-\r
-    def patron_info(self, barcode):\r
-        msg = self.send(PATRON_INFO,PATRON_INFO_RESP,\r
-                        {'patron':barcode,\r
-                         'startitem':1, 'enditem':2})\r
-        # fixme, this may not be the best test of okayness\r
-        msg['success'] = msg.get('valid_patron') == 'Y'\r
-        return msg\r
-\r
-    def checkout(self, patron, item, inst=''):\r
-        msg = self.send(CHECKOUT, CHECKOUT_RESP,\r
-                        {'patron':patron,\r
-                         'inst': inst,\r
-                         'item':item})\r
-        msg['media_type'] = MEDIA_TYPE_TABLE.get(msg.get('media_type_code'))\r
-        msg['success'] = msg.get('ok') == '1'\r
-        return msg\r
-\r
-    def checkin(self, item, institution='', location=''):\r
-        msg = self.send(CHECKIN, CHECKIN_RESP,\r
-                        {'inst': institution,\r
-                         'location':location,\r
-                         'is_retry':False,\r
-                         'item':item})\r
-        msg['success'] = msg.get('ok') == '1'\r
-        return msg\r
-\r
-    def item_info(self, barcode):\r
-        print("starting")\r
-        msg = self.send(ITEM_INFORMATION, ITEM_INFO_RESP,\r
-                        {'item':barcode})\r
-        print(msg['circstat'])\r
-        msg['available'] = msg['circstat'] == '03'\r
-        msg['status'] = ITEM_STATUS_TABLE[msg['circstat']]\r
-        return msg\r
-\r
-\r
-# ------------------------------------------------------------\r
-# Django stuff. Optional.\r
-\r
-try:\r
-    from django.conf import settings\r
-    def sip_connection():\r
-        sip = SipClient(*settings.SIP_HOST)\r
-        if not sip.login(*settings.SIP_CREDENTIALS):\r
-            raise 'SipLoginError'\r
-        return sip\r
-\r
-    # decorator\r
-    def SIP(fn):\r
-        def f(*args, **kwargs):\r
-            conn = sip_connection()\r
-            resp = fn(conn, *args, **kwargs)\r
-            conn.close()\r
-            return resp\r
-        return f\r
-\r
-except ImportError:\r
-    pass\r
-\r
-\r
-# ------------------------------------------------------------\r
-# Test code.\r
-\r
-if __name__ == '__main__':\r
-    from pprint import pprint\r
-\r
-    sip = SipClient('comet.cs.uoguelph.ca', 8080)\r
-    resp = sip.login(uid='test',\r
-                     pwd='test', locn='test')\r
-    pprint(resp)\r
-    pprint(sip.status())\r
-\r
-    pprint(sip.send(PATRON_INFO, PATRON_INFO_RESP,\r
-                   {'patron':'scclient',\r
-                    'startitem':1, 'enditem':2}))\r
-\r
-    # these are items from openncip's test database.\r
-    item_ids = ['1565921879', '0440242746', '660']\r
-    bad_ids = ['xx' + i for i in item_ids]\r
-    for item in (item_ids + bad_ids):\r
-        result = sip.send(ITEM_INFORMATION, ITEM_INFO_RESP,\r
-                          {'item':item})\r
-        print '%-12s: %s' % (item, result['title'] or '????')\r
-        print sip.send(CHECKOUT, RAW,\r
-                       {'patron':'scclient-2',\r
-                        'inst': 'UWOLS',\r
-                        'item':item})\r
-        print '\n' * 5\r
-    pprint(sip.send(END_PATRON_SESSION, END_SESSION_RESP,\r
-                   {'patron':'scclient',\r
-                    'inst':'UWOLS'}))\r
-\r
-\r
+# 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'}))
+
+
index bd2bfc3..09a951f 100644 (file)
-# z39.50 search using yaz-client. \r
-# dependencies: yaz-client, pexpect\r
-\r
-# I found that pyz3950.zoom seemed wonky when testing against conifer\r
-# z3950, so I whipped up this expect-based version instead.\r
-\r
-import warnings\r
-import re\r
-import sys\r
-from marcxml import marcxml_to_dictionary\r
-\r
-try:\r
-\r
-    import profile\r
-    import lex\r
-    import yacc\r
-except ImportError:\r
-\r
-    sys.modules['profile'] = sys # just get something called 'profile';\r
-                                     # it's not actually used.\r
-    import ply.lex\r
-    import ply.yacc             # pyz3950 thinks these are toplevel modules.\r
-    sys.modules['lex'] = ply.lex\r
-    sys.modules['yacc'] = ply.yacc\r
-\r
-# for Z39.50 support, not sure whether this is the way to go yet but\r
-# as generic as it gets\r
-from PyZ3950 import zoom, zmarc\r
-\r
-\r
-LOG = None              #  for pexpect debugging, try LOG = sys.stderr\r
-GENERAL_TIMEOUT = 40\r
-PRESENT_TIMEOUT = 60\r
-\r
-def search(host, port, database, query, start=1, limit=10):\r
-\r
-\r
-    query = query.encode('utf-8') # is this okay? Is it enough??\r
-\r
-    conn = zoom.Connection(host, port)\r
-    conn.databaseName = database\r
-    conn.preferredRecordSyntax = 'XML'\r
-    \r
-    query = zoom.Query ('CCL', str(query))\r
-    res = conn.search (query)\r
-    collector = []\r
-    #if we were dealing with marc8 results, would probably need this\r
-    #m = zmarc.MARC8_to_Unicode ()\r
-\r
-    # how many to present? At most 10 for now.\r
-    to_show = min(len(res)-(start - 1), limit)\r
-    if limit:\r
-        to_show = min(to_show, limit)\r
-\r
-\r
-    #this seems to an efficient way of snagging the records\r
-    #would be good to cache the result set for iterative display\r
-    for r in range(start - 1,(start-1) + to_show):\r
-        #would need to translate marc8 records, evergreen doesn't need this\r
-        #collector.append(m.translate(r.data))\r
-        collector.append(str(res.__getitem__(r)).replace('\n',''))\r
-    conn.close ()\r
-\r
-\r
-    raw = "" . join(collector)\r
-\r
-    raw_records = []\r
-    err = None\r
-\r
-    pat = re.compile('<record .*?</record>', re.M)\r
-    raw_records = pat.findall(raw)\r
-\r
-    parsed = []\r
-    for rec in raw_records:\r
-        try:\r
-            rec = _marc_utf8_pattern.sub(_decode_marc_utf8, rec)\r
-            dct = marcxml_to_dictionary(rec)\r
-        except 'x':\r
-            raise rec\r
-        parsed.append(dct)\r
-    return parsed, len(res)\r
-\r
-\r
-# decoding MARC \X.. UTF-8 patterns.\r
-\r
-_marc_utf8_pattern = re.compile(r'\\X([0-9A-F]{2})')\r
-\r
-def _decode_marc_utf8(regex_match):\r
-    return chr(int(regex_match.group(1), 16))\r
-\r
-\r
-#------------------------------------------------------------\r
-# some tests\r
-\r
-if __name__ == '__main__':\r
-    tests = [\r
-        ('zed.concat.ca:210', 'OSUL', 'chanson'),\r
-        ]\r
-    for host, db, query in tests:\r
-        print (host, db, query)\r
-        print len(search(host, db, query, limit=33))\r
+# 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))
index 7682924..439d05a 100644 (file)
-# Django settings for conifer project.\r
-\r
-import os\r
-\r
-os.environ['PYTHON_EGG_CACHE'] = '/tmp/eggs'\r
-\r
-BASE_DIRECTORY = os.path.abspath(os.path.dirname(__file__))\r
-HERE = lambda s: os.path.join(BASE_DIRECTORY, s)\r
-\r
-DEBUG = True\r
-TEMPLATE_DEBUG = DEBUG\r
-\r
-ADMINS = (\r
-    # ('Your Name', 'your_email@domain.com'),\r
-)\r
-\r
-MANAGERS = ADMINS\r
-\r
-DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.\r
-DATABASE_NAME = HERE('syrup.sqlite') # Or path to database file if using sqlite3.\r
-DATABASE_USER = ''             # Not used with sqlite3.\r
-DATABASE_PASSWORD = ''         # Not used with sqlite3.\r
-DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.\r
-DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.\r
-\r
-# Local time zone for this installation. Choices can be found here:\r
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name\r
-# although not all choices may be available on all operating systems.\r
-# If running in a Windows environment this must be set to the same as your\r
-# system time zone.\r
-TIME_ZONE = 'America/Detroit'\r
-\r
-# Language code for this installation. All choices can be found here:\r
-# http://www.i18nguy.com/unicode/language-identifiers.html\r
-LANGUAGE_CODE = 'en_US'\r
-\r
-# Please only include languages here for which we have a locale in our\r
-# locale/ directory.\r
-LANGUAGES = [("en-us", "English"),\r
-             ("fr-ca", "Canadian French"),\r
-             ]\r
-\r
-SITE_ID = 1\r
-\r
-# If you set this to False, Django will make some optimizations so as not\r
-# to load the internationalization machinery.\r
-USE_I18N = True\r
-\r
-# Absolute path to the directory that holds media.\r
-# Example: "/home/media/media.lawrence.com/"\r
-MEDIA_ROOT = HERE('static')\r
-\r
-# URL that handles the media served from MEDIA_ROOT. Make sure to use a\r
-# trailing slash if there is a path component (optional in other cases).\r
-# Examples: "http://media.lawrence.com", "http://example.com/media/"\r
-MEDIA_URL = ''\r
-\r
-# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a\r
-# trailing slash.\r
-# Examples: "http://foo.com/media/", "/media/".\r
-ADMIN_MEDIA_PREFIX = '/syrup/djmedia/'\r
-\r
-# Make this unique, and don't share it with anybody.\r
-SECRET_KEY = 'j$dnxqbi3iih+(@il3m@vv(tuvt2+yu2r-$dxs$s7=iqjz_s!&'\r
-\r
-# List of callables that know how to import templates from various sources.\r
-TEMPLATE_LOADERS = (\r
-    'django.template.loaders.filesystem.load_template_source',\r
-    'django.template.loaders.app_directories.load_template_source',\r
-#     'django.template.loaders.eggs.load_template_source',\r
-)\r
-\r
-MIDDLEWARE_CLASSES = (\r
-    'django.middleware.common.CommonMiddleware',\r
-    'django.contrib.sessions.middleware.SessionMiddleware',\r
-    'django.contrib.auth.middleware.AuthenticationMiddleware',\r
-    'conifer.middleware.genshi_locals.ThreadLocals',\r
-    'django.middleware.locale.LocaleMiddleware',\r
-    'babeldjango.middleware.LocaleMiddleware',\r
-    # TransactionMiddleware should be last...\r
-    'django.middleware.transaction.TransactionMiddleware',\r
-)\r
-\r
-ROOT_URLCONF = 'conifer.urls'\r
-\r
-TEMPLATE_DIRS = []\r
-\r
-INSTALLED_APPS = (\r
-    'django.contrib.auth',\r
-    'django.contrib.contenttypes',\r
-    'django.contrib.sessions',\r
-    'django.contrib.sites',\r
-    'django.contrib.admin',\r
-    'conifer.syrup',\r
-)\r
-\r
-AUTH_PROFILE_MODULE = 'syrup.UserProfile'\r
-\r
-\r
-AUTHENTICATION_BACKENDS = (\r
-    'django.contrib.auth.backends.ModelBackend',\r
-    # uncomment for EG authentication:\r
-    #'conifer.custom.auth_evergreen.EvergreenAuthBackend',\r
-)\r
-\r
-\r
-EVERGREEN_GATEWAY_SERVER = 'www.concat.ca'\r
-Z3950_CONFIG = ('zed.concat.ca', 210, 'OWA')  #OWA,OSUL,CONIFER\r
-SIP_HOST = ('localhost', 8080)\r
-\r
-try:\r
-    from private_local_settings import SIP_CREDENTIALS\r
-except:\r
-    # stuff that I really ought not check into svn...\r
-    SIP_CREDENTIALS = ('test', 'test', 'test')\r
-    pass\r
-\r
-\r
-#CACHE_BACKEND = 'memcached://127.0.0.1:11211/'\r
-#CACHE_BACKEND = 'db://test_cache_table'\r
-#CACHE_BACKEND = 'locmem:///'\r
+# Django settings for conifer project.
+
+import os
+
+os.environ['PYTHON_EGG_CACHE'] = '/tmp/eggs'
+
+BASE_DIRECTORY = os.path.abspath(os.path.dirname(__file__))
+HERE = lambda s: os.path.join(BASE_DIRECTORY, s)
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+DATABASE_NAME = HERE('syrup.sqlite') # Or path to database file if using sqlite3.
+DATABASE_USER = ''             # Not used with sqlite3.
+DATABASE_PASSWORD = ''         # Not used with sqlite3.
+DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Detroit'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en_US'
+
+# Please only include languages here for which we have a locale in our
+# locale/ directory.
+LANGUAGES = [("en-us", "English"),
+             ("fr-ca", "Canadian French"),
+             ]
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = HERE('static')
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/syrup/djmedia/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'j$dnxqbi3iih+(@il3m@vv(tuvt2+yu2r-$dxs$s7=iqjz_s!&'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+#     'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'conifer.middleware.genshi_locals.ThreadLocals',
+    'django.middleware.locale.LocaleMiddleware',
+    'babeldjango.middleware.LocaleMiddleware',
+    # TransactionMiddleware should be last...
+    'django.middleware.transaction.TransactionMiddleware',
+)
+
+ROOT_URLCONF = 'conifer.urls'
+
+TEMPLATE_DIRS = []
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.admin',
+    'conifer.syrup',
+)
+
+AUTH_PROFILE_MODULE = 'syrup.UserProfile'
+
+
+AUTHENTICATION_BACKENDS = (
+    'django.contrib.auth.backends.ModelBackend',
+    # uncomment for EG authentication:
+    #'conifer.custom.auth_evergreen.EvergreenAuthBackend',
+)
+
+
+EVERGREEN_GATEWAY_SERVER = 'www.concat.ca'
+Z3950_CONFIG = ('zed.concat.ca', 210, 'OWA')  #OWA,OSUL,CONIFER
+SIP_HOST = ('localhost', 8080)
+
+try:
+    from private_local_settings import SIP_CREDENTIALS
+except:
+    # stuff that I really ought not check into svn...
+    SIP_CREDENTIALS = ('test', 'test', 'test')
+    pass
+
+
+#CACHE_BACKEND = 'memcached://127.0.0.1:11211/'
+#CACHE_BACKEND = 'db://test_cache_table'
+#CACHE_BACKEND = 'locmem:///'
index 21bb4d1..8373aaf 100644 (file)
@@ -1,19 +1,19 @@
-/*\r
-this seems to be causing a disable when we don't want it\r
-*/\r
-function do_init() {\r
-    if ($('#id_code')[0].tagName == 'SELECT') {\r
-       // code is a SELECT, so we add a callback to lookup titles.\r
-       $('#id_code').change(function() {\r
-           $('#id_title')[0].disabled=true;\r
-           $.getJSON('/syrup/course/new/ajax_title', {course_code: $(this).val()},\r
-                     function(resp) {\r
-                         $('#id_title').val(resp.title)\r
-                         $('#id_title')[0].disabled=false;\r
-\r
-                     });\r
-       });\r
-    }\r
-}\r
-\r
-$(do_init);\r
+/*
+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);
index 529e5dd..4585414 100644 (file)
@@ -1,43 +1,43 @@
-/*\r
- * jQuery UI 1.7.1\r
- *\r
- * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)\r
- * Dual licensed under the MIT (MIT-LICENSE.txt)\r
- * and GPL (GPL-LICENSE.txt) licenses.\r
- *\r
- * http://docs.jquery.com/UI\r
- */\rjQuery.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);;/*\r
- * jQuery UI Draggable 1.7.1\r
- *\r
- * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)\r
- * Dual licensed under the MIT (MIT-LICENSE.txt)\r
- * and GPL (GPL-LICENSE.txt) licenses.\r
- *\r
- * http://docs.jquery.com/UI/Draggables\r
- *\r
- * Depends:\r
- *     ui.core.js\r
- */\r(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);;/*\r
- * jQuery UI Droppable 1.7.1\r
- *\r
- * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)\r
- * Dual licensed under the MIT (MIT-LICENSE.txt)\r
- * and GPL (GPL-LICENSE.txt) licenses.\r
- *\r
- * http://docs.jquery.com/UI/Droppables\r
- *\r
- * Depends:\r
- *     ui.core.js\r
- *     ui.draggable.js\r
- */\r(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);;/*\r
- * jQuery UI Sortable 1.7.1\r
- *\r
- * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)\r
- * Dual licensed under the MIT (MIT-LICENSE.txt)\r
- * and GPL (GPL-LICENSE.txt) licenses.\r
- *\r
- * http://docs.jquery.com/UI/Sortables\r
- *\r
- * Depends:\r
- *     ui.core.js\r
+/*
+ * 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
+ */\rjQuery.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
+ */\r(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
+ */\r(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
  */\r(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
index 49e7795..97a5c83 100644 (file)
@@ -536,156 +536,156 @@ function evalScript( i, elem ) {
 function now() {
        return (new Date).getTime();
 }
-var expando = "jQuery" + now(), uuid = 0, windowData = {};\r
-\r
-jQuery.extend({\r
-       cache: {},\r
-\r
-       data: function( elem, name, data ) {\r
-               elem = elem == window ?\r
-                       windowData :\r
-                       elem;\r
-\r
-               var id = elem[ expando ], cache = jQuery.cache;\r
-\r
-               // Compute a unique ID for the element\r
-               if(!id) id = elem[ expando ] = ++uuid;\r
-\r
-               // Only generate the data cache if we're\r
-               // trying to access or manipulate it\r
-               if ( name && !cache[ id ] )\r
-                       cache[ id ] = {};\r
-\r
-               var thisCache = cache[ id ];\r
-\r
-               // Prevent overriding the named cache with undefined values\r
-               if ( data !== undefined ) thisCache[ name ] = data;\r
-\r
-               if(name === true) return thisCache\r
-               else if(name) return thisCache[name]\r
-               else return id\r
-       },\r
-\r
-       removeData: function( elem, name ) {\r
-               elem = elem == window ?\r
-                       windowData :\r
-                       elem;\r
-\r
-               var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ];\r
-\r
-               // If we want to remove a specific section of the element's data\r
-               if ( name ) {\r
-                       if ( thisCache ) {\r
-                               // Remove the section of cache data\r
-                               delete thisCache[ name ];\r
-\r
-                               // If we've removed all the data, remove the element's cache\r
-                               if( jQuery.isEmptyObject(thisCache) )\r
-                                       jQuery.removeData( elem );\r
-                       }\r
-\r
-               // Otherwise, we want to remove all of the element's data\r
-               } else {\r
-                       // Clean up the element expando\r
-                       try {\r
-                               delete elem[ expando ];\r
-                       } catch(e){\r
-                               // IE has trouble directly removing the expando\r
-                               // but it's ok with using removeAttribute\r
-                               if ( elem.removeAttribute )\r
-                                       elem.removeAttribute( expando );\r
-                       }\r
-\r
-                       // Completely remove the data cache\r
-                       delete cache[ id ];\r
-               }\r
-       },\r
-       queue: function( elem, type, data ) {\r
-               if( !elem ) return;\r
-\r
-               type = (type || "fx") + "queue";\r
-               var q = jQuery.data( elem, type );\r
-\r
-               // Speed up dequeue by getting out quickly if this is just a lookup\r
-               if( !data ) return q || [];\r
-\r
-               if ( !q || jQuery.isArray(data) )\r
-                       q = jQuery.data( elem, type, jQuery.makeArray(data) );\r
-               else\r
-                       q.push( data );\r
-\r
-               return q;\r
-       },\r
-\r
-       dequeue: function( elem, type ){\r
-               type = type || "fx";\r
-\r
-               var queue = jQuery.queue( elem, type ), fn = queue.shift();\r
-\r
-               // If the fx queue is dequeued, always remove the progress sentinel\r
-               if( fn === "inprogress" ) fn = queue.shift();\r
-\r
-               if( fn ) {\r
-                       // Add a progress sentinel to prevent the fx queue from being\r
-                       // automatically dequeued\r
-                       if( type == "fx" ) queue.unshift("inprogress");\r
-\r
-                       fn.call(elem, function() { jQuery.dequeue(elem, type); });\r
-               }\r
-       }\r
-});\r
-\r
-jQuery.fn.extend({\r
-       data: function( key, value ){\r
-               if(typeof key === "undefined" && this.length) return jQuery.data(this[0], true);\r
-\r
-               var parts = key.split(".");\r
-               parts[1] = parts[1] ? "." + parts[1] : "";\r
-\r
-               if ( value === undefined ) {\r
-                       var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);\r
-\r
-                       if ( data === undefined && this.length )\r
-                               data = jQuery.data( this[0], key );\r
-\r
-                       return data === undefined && parts[1] ?\r
-                               this.data( parts[0] ) :\r
-                               data;\r
-               } else\r
-                       return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){\r
-                               jQuery.data( this, key, value );\r
-                       });\r
-       },\r
-\r
-       removeData: function( key ){\r
-               return this.each(function(){\r
-                       jQuery.removeData( this, key );\r
-               });\r
-       },\r
-       queue: function(type, data){\r
-               if ( typeof type !== "string" ) {\r
-                       data = type;\r
-                       type = "fx";\r
-               }\r
-\r
-               if ( data === undefined )\r
-                       return jQuery.queue( this[0], type );\r
-\r
-               return this.each(function(i, elem){\r
-                       var queue = jQuery.queue( this, type, data );\r
-\r
-                       if( type == "fx" && queue[0] !== "inprogress" )\r
-                               jQuery.dequeue( this, type )\r
-               });\r
-       },\r
-       dequeue: function(type){\r
-               return this.each(function(){\r
-                       jQuery.dequeue( this, type );\r
-               });\r
-       },\r
-       clearQueue: function(type){\r
-               return this.queue( type || "fx", [] );\r
-       }\r
+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
index 6306da2..f9c1af7 100644 (file)
-html, body, div, span, applet, object, iframe,\r
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,\r
-a, abbr, acronym, address, big, cite, code,\r
-del, dfn, em, font, img, ins, kbd, q, s, samp,\r
-small, strike, strong, sub, sup, tt, var,\r
-dl, dt, dd, ol, ul, li,\r
-fieldset, form, label, legend,\r
-table, caption, tbody, tfoot, thead, tr, th, td {\r
-       margin: 0;\r
-       padding: 0;\r
-       border: 0;\r
-       outline: 0;\r
-       font-weight: inherit;\r
-       font-style: inherit;\r
-       font-size: 100%;\r
-       font-family: inherit;\r
-       vertical-align: baseline;\r
-}\r
-/* remember to define focus styles! */\r
-:focus {\r
-       outline: 0;\r
-}\r
-body {\r
-       line-height: 1;\r
-       color: black;\r
-       background: white;\r
-}\r
-ol, ul {\r
-       list-style: none;\r
-}\r
-/* tables still need 'cellspacing="0"' in the markup */\r
-table {\r
-       border-collapse: separate;\r
-       border-spacing: 0;\r
-}\r
-caption, th, td {\r
-       text-align: left;\r
-       font-weight: normal;\r
-}\r
-blockquote:before, blockquote:after,\r
-q:before, q:after {\r
-       content: "";\r
-}\r
-blockquote, q {\r
-       quotes: "" "";\r
-}\r
-\r
-/* General look and feel */\r
-\r
-body * {  font-family: Verdana, sans-serif; }\r
-pre, code { font-family: monospace; }\r
-\r
-body, html { margin: 0; padding: 0; }\r
-\r
-body {  \r
-    background-color: #005; \r
-    font-size: normal; \r
-}\r
-\r
-div#outer {\r
-    background-color: white; \r
-    width: 960px; margin-bottom: 50px;\r
-    margin-left: auto; margin-right: auto;\r
-}\r
-#mainpanel {  background-color: white; padding: 0 12px 24px 12px; \r
-min-height: 300px; \r
-}\r
-\r
-/* General headers and footers */\r
-\r
-#header, #footer {\r
-    color: white; padding: 8px 4px 12px 8px;\r
-    background-color: #448;\r
-}\r
-\r
-#header div#search {\r
-    float: right;\r
-    margin: 0;\r
-    color: #ccc;\r
-}\r
-\r
-#header #welcome {\r
-    margin-top: 4px;\r
-}\r
-\r
-#brandheader { background-color: white; padding: 8px; }\r
-\r
-#header a { color: #fff; font-weight: bold;  padding: 10px 12px 10px 12px; }\r
-#header a.loginbutton { background-color: #a44; }\r
-#header a:hover { background-color: #fb7; color: black; text-decoration: none; }\r
-\r
-tbody td, tbody th { vertical-align: top; }\r
-\r
-#footer {  \r
-    margin: 12px;\r
-    padding-bottom: 12px;\r
-    background-color: #ddf; \r
-    color: black;\r
-    border-bottom: white 10px solid;\r
-}\r
-\r
-/* heading sizes and colours. */\r
-\r
-h1 { font-size: 150%; font-weight: bold; }\r
-h2 { font-size: 125%; font-weight: bold; }\r
-h3 { font-size: 120%; font-weight: bold; }\r
-p { margin: 24px 0px; }\r
-h1 { color: navy; margin: 36px 0 18px 0; }\r
-h2 { color: #336; margin: 24px 0 12px 0; }\r
-h3, h4 { color: darkgreen; }\r
-h1 a, h2 a { color: navy; }\r
-\r
-a { color: blue; text-decoration: none; }\r
-a:hover {  text-decoration: underline;  }\r
-\r
-/* error panel on the person-edit form */\r
-\r
-.errors {  margin: 1em; padding: 1em; background-color: #fdd; }\r
-.errors h2 { font-size: 120%; }\r
-\r
-/* actions (e.g. "edit user" link) */\r
-.action a { font-weight: bold; }\r
-\r
-#tabbar { margin: 18px 0; padding: 0; clear: both; }\r
-#tabbar li { display: inline; }\r
-#tabbar li a { padding: 15px 18px 5px 18px; background-color: #ddf; color: black; text-decoration: none; }\r
-#tabbar li a:hover { background-color: #fc8; }\r
-\r
-/* \r
-#tabbar li.active a { background-color: #fa6; font-weight: bold; }\r
-*/\r
-\r
-.pagination_controls {\r
-    text-align: center; margin: 12px 0;\r
-}\r
-\r
-.pagination_controls .nums {\r
-    padding: 0 12px; \r
-}\r
-\r
-.pagetable td { border: #ddd 1px solid; padding: 8px; }\r
-.pagetable .odd {\r
-    background-color: #F8F8F8;\r
-}\r
-.pagetable thead th { font-size: smaller; text-align: left; padding: 2px 8px; }\r
-\r
-\r
-/* nested titles: like breadcrumbs when drilling into an itemtree */\r
-\r
-.nestedtitle h2 { margin-top: 8px; }\r
-.nestedtitle a { color: navy; }\r
-\r
-span.final_item { font-weight: bold; font-size: 110%; }\r
-\r
-/* item trees (tree of headings and items in a course */\r
-\r
-#sidepanel { width: 183px; float: right; text-align: right;}\r
-#sidepanel div { margin: 6px 0; }\r
-\r
-#treepanel { width: 740px; }\r
-\r
-.helptext { \r
-    margin-top: 30px; font-size: 90%;\r
-    padding: 10px; \r
-    background-color: #eef; \r
-    clear: both;\r
-}\r
-\r
-.itemtree { \r
-    margin-left: 20px;\r
-    padding-left: 20px;\r
-    list-style-type: none; \r
-}\r
-\r
-.itemtree .itemtree { margin-left: 30px; padding-left: 0; }\r
-\r
-.itemtree li { padding-left: 0; margin-left: 0; } \r
-\r
-.itemtree li { margin: 12px 8px; }\r
-.itemtree li .mainline { padding-left: 8px; }\r
-\r
-.itemtree .metalink { padding-left: 8px; color: gray; }\r
-.itemtree .metalink a {\r
-    color: gray; \r
-}\r
-\r
-.itemtree .editlinks   { padding-left: 12px; color: gray; \r
-                        font-size: small;\r
-                    }\r
-.itemtree .editlinks a { color: navy; }\r
-\r
-.itemadd { \r
-    margin-top: 30px; font-size: 90%;\r
-    padding: 10px; \r
-    background-color: #eef; \r
-    clear: both;\r
-}\r
-.itemadd li {     \r
-    margin: 10px; \r
-}\r
-\r
-.itemadd a { color: navy; }\r
-\r
-/* specialized display of items in tree, by type */\r
-\r
-.itemtree li.item_HEADING { \r
-    list-style-image: url(tango/folder.png);\r
-}\r
-.itemtree li.item_HEADING > a { \r
-    color: navy; \r
-}\r
-\r
-li.item_HEADING .headingmainline {\r
-    margin-bottom: 12px;\r
-}\r
-\r
-li.item_HEADING .headingmainline  a.mainlink {\r
-    border-bottom: #aaa 1px solid; \r
-}\r
-\r
-li.item_HEADING  .headingmainline a.mainlink:hover {\r
-    border-bottom: none;\r
-}\r
-\r
-.itemtree li.item_ELEC { \r
-    list-style-image: url(tango/document.png);\r
-}\r
-\r
-.itemtree li.item_URL { \r
-    list-style-image: url(tango/applications-internet.png);\r
-}\r
-\r
-.itemtree li.item_PHYS { \r
-    /* fixme: need a better icon */\r
-    list-style-image: url(tango/x-office-address-book.png);\r
-}\r
-\r
-\r
-.instructors {\r
-  border: 1px solid #ccc;\r
-  float: left;\r
-  width: 50%;\r
-  padding: 2px 2px 2px 2px;\r
-  font-size: 1em;\r
-  line-height: 1em;\r
-  text-align: left;\r
-  margin-right: 5px;\r
-}\r
-\r
-.topbox {\r
-  border: 1px solid #ccc;\r
-  width: 50%;\r
-  line-height: 1em;\r
-  text-align: left;\r
-}\r
-\r
-table.topheading { width: 100%; }\r
-.topheading th {\r
-    background-color: #ddf;\r
-}\r
-\r
-.topheading th, .topheading td {\r
-padding: 8px;\r
-}\r
-\r
-p.todo, div.todo { background-color: #fdd; padding: 6px; margin: 12px; border-left: #d99 6px solid; }\r
-\r
-.newsitem p { margin: 12px 0; }\r
-.newsitem ul { list-style: circle; margin-left: 30px; }\r
-.newsitem ul li { margin: 8px; }\r
-\r
-.newsitem { \r
-    max-width: 600px;\r
-    line-height: 125%;\r
-}\r
-.newsitem .newsdate { \r
-    margin: 4px 0 8px 0; text-align: right; \r
-    font-size: 80%; color: navy;\r
-}\r
-\r
-.menublockopener { margin-left: 0.25em; color: #bbb !important; font-weight: normal !important; }\r
-.menublock { background-color: #f2e4cc; font-size: 95%; padding: 1px 4px; }\r
-\r
-#coursebanner { background-color: #f2e4cc; margin: -12px -12px 12px -12px; padding: 8px; }\r
-#coursesearch { float: right; }\r
-#coursebanner h1 { margin: 12px 0; font-size: 125%; }\r
-\r
-#edit_course_link { margin: 8px 0 8px 0; font-size: 95%; }\r
-\r
-\r
-#breadcrumbs { margin: 8px 0px 16px 0; width: 716px;  \r
-            text-indent: -24px; padding-left: 24px; }\r
-\r
-.errorlist { float: right; }\r
-.errorlist li { color: red; font-size: 90%; }\r
-\r
-\r
-/* a nice table-style for forms. */\r
-.formtable tbody th { \r
-    padding: 0 8px 16px 0;\r
-    width: 200px; \r
-    text-align: left; \r
-    font-size: 90%;\r
-    font-weight: normal; \r
-}\r
-\r
-thead th { padding: 8px; font-weight: bold; font-size: 90%; }\r
-.metadata_table tbody th,\r
-.metadata_table tbody td {\r
-    padding: 8px; border: #ddd 1px solid;\r
-}\r
-.metadata_table input { width: 600px; }\r
-.metadata_table .meta3 input { width: 10px; }\r
-\r
-.metadata_table tbody th {\r
-    background-color: #eee;\r
-}\r
-   \r
-.metadata_table a.bigdownload { padding: 8px 58px; font-weight: bold; font-size: 105%; }\r
-.metadata_table a.bigdownload:hover { background-color: #dfd; color: black; }\r
-\r
-h2.metadata_subhead {font-size: 105%; padding: 0; margin: 18px 0 9px 0;}\r
-\r
-.metadata_table tbody th {\r
-    text-align: left; width: 120px;\r
-}\r
-.gap { height: 24px; }\r
-.metadata_table td { max-width: 800px; overflow: hidden; }\r
-\r
-/* panels that appear when specific OPTIONs or radio-buttons are selected. */\r
-.specific { padding: 8px; margin: 0 16px; background-color: #eef; }\r
-\r
-\r
-li.sort_item { margin-top: 20px !important;\r
-            border: gray 1px dotted; width: 400px; }\r
-\r
-li.sort_item:hover { background-color: #eee; }\r
-\r
-ul.heading_tree li  { list-style: none; }\r
-ul.heading_tree { margin: 0; padding-left: 0; }\r
-ul.heading_tree ul { margin: 0; padding-left: 25px; }\r
-\r
+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; }
+
index b47520c..6f145d8 100644 (file)
@@ -1,18 +1,18 @@
-<?xml version="1.0" encoding="UTF-8"?>\r
-<!--\r
-libxslt needs explicit namespaces\r
--->\r
-<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"\r
-xmlns:marc="http://www.loc.gov/MARC21/slim"  version="1.0">\r
-\r
-<xsl:template match="text()"/>\r
-\r
-<xsl:template match="/marc:record">\r
-        <xsl:apply-templates/>\r
-</xsl:template>\r
-\r
-<xsl:template match="marc:datafield[@tag='245']">\r
-<xsl:value-of select="marc:subfield[@code='a']"/>\r
-</xsl:template>\r
-\r
+<?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
index 0a28079..66c50f5 100644 (file)
@@ -1,63 +1,63 @@
-from django.conf.urls.defaults import *\r
-\r
-# I'm not ready to break items out into their own urls.py, but I do\r
-# want to cut down on the common boilerplate in the urlpatterns below.\r
-\r
-ITEM_PREFIX = r'^course/(?P<course_id>\d+)/item/(?P<item_id>\d+)/'\r
-GENERIC_REGEX = r'((?P<obj_id>\d+)/)?(?P<action>.+)?$'\r
-\r
-urlpatterns = patterns('conifer.syrup.views',\r
-    (r'^$', 'welcome'),                       \r
-    (r'^course/$', 'my_courses'),\r
-    (r'^course/new/$', 'add_new_course'),\r
-    (r'^course/new/ajax_title$', 'add_new_course_ajax_title'),\r
-    (r'^course/invitation/$', 'course_invitation'),\r
-    (r'^browse/$', 'browse'),\r
-    (r'^browse/(?P<browse_option>.*)/$', 'browse'),\r
-    (r'^prefs/$', 'user_prefs'),\r
-    (r'^z3950test/$', 'z3950_test'),\r
-    #MARK: propose we kill open_courses, we have browse.\r
-    (r'^opencourse/$', 'open_courses'),\r
-    (r'^search/$', 'search'),\r
-    (r'^zsearch/$', 'zsearch'),\r
-    #MARK: propose we kill instructors, we have browse\r
-    (r'^instructors/$', 'instructors'),\r
-    (r'^instructors/search/(?P<instructor>.*)$', 'instructor_search'),\r
-    #MARK: propose we kill departments, we have browse\r
-    (r'^departments/$', 'departments'),\r
-    (r'^course/(?P<course_id>\d+)/$', 'course_detail'),\r
-    (r'^instructor/(?P<instructor_id>.*)/$', 'instructor_detail'),\r
-    (r'^department/(?P<department_id>.*)/$', 'department_detail'),\r
-    (r'^course/(?P<course_id>\d+)/search/$', 'course_search'),\r
-    (r'^course/(?P<course_id>\d+)/edit/$', 'edit_course'),\r
-    (r'^course/(?P<course_id>\d+)/edit/delete/$', 'delete_course'),\r
-    (r'^course/(?P<course_id>\d+)/edit/permission/$', 'edit_course_permissions'),\r
-    (r'^course/(?P<course_id>\d+)/feeds/(?P<feed_type>.*)$', 'course_feeds'),\r
-    (r'^course/(?P<course_id>\d+)/join/$', 'course_join'),\r
-    (ITEM_PREFIX + r'$', 'item_detail'),\r
-    (ITEM_PREFIX + r'dl/(?P<filename>.*)$', 'item_download'),\r
-    (ITEM_PREFIX + r'meta$', 'item_metadata'),\r
-    (ITEM_PREFIX + r'edit/$', 'item_edit'),\r
-    (ITEM_PREFIX + r'delete/$', 'item_delete'),\r
-    (ITEM_PREFIX + r'add/$', 'item_add'), # for adding sub-things\r
-    (ITEM_PREFIX + r'add/cat_search/$', 'item_add_cat_search'),\r
-\r
-    (r'^admin/$', 'admin_index'),\r
-    (r'^admin/terms/' + GENERIC_REGEX, 'admin_terms'),\r
-    (r'^admin/depts/' + GENERIC_REGEX, 'admin_depts'),\r
-    (r'^admin/news/' + GENERIC_REGEX, 'admin_news'),\r
-    (r'^admin/targets/' + GENERIC_REGEX, 'admin_targets'),\r
-\r
-    (r'^phys/$', 'phys_index'),\r
-    (r'^phys/checkout/$', 'phys_checkout'),\r
-    (r'^phys/mark_arrived/$', 'phys_mark_arrived'),\r
-    (r'^phys/mark_arrived/match/$', 'phys_mark_arrived_match'),\r
-    (r'^phys/circlist/$', 'phys_circlist'),\r
-\r
-    (r'^course/(?P<course_id>\d+)/reseq$', 'course_reseq'),\r
-    (ITEM_PREFIX + r'reseq', 'item_heading_reseq'),\r
-    (ITEM_PREFIX + r'relocate/', 'item_relocate'), # move to new subheading\r
-#     (r'^admin/terms/(?P<term_id>\d+)/$', 'admin_term_edit'),\r
-#     (r'^admin/terms/(?P<term_id>\d+)/delete$', 'admin_term_delete'),\r
-#     (r'^admin/terms/$', 'admin_term'),\r
-)\r
+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'),
+)
index 633a536..8c49001 100644 (file)
-from _common import *\r
-from django.utils.translation import ugettext as _\r
-from search import *\r
-#from lxml import etree\r
-#import libxml2\r
-#import libxslt\r
-import os\r
-\r
-\r
-BASE_DIRECTORY = os.path.abspath(os.path.dirname(__file__))\r
-HERE = lambda s: os.path.join(BASE_DIRECTORY, s)\r
-\r
-\r
-#-----------------------------------------------------------------------------\r
-\r
-def welcome(request):\r
-    return g.render('welcome.xhtml')\r
-\r
-# MARK: propose we get rid of this. We already have a 'Courses' browser.\r
-def open_courses(request):\r
-    page_num = int(request.GET.get('page', 1))\r
-    count = int(request.GET.get('count', 5))\r
-    paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter?\r
-    return g.render('open_courses.xhtml', paginator=paginator,\r
-                    page_num=page_num,\r
-                    count=count)\r
-# MARK: propose we drop this too. We have a browse.\r
-def instructors(request):\r
-    page_num = int(request.GET.get('page', 1))\r
-    count = int(request.GET.get('count', 5))\r
-    action = request.GET.get('action', 'browse')\r
-    if action == 'join':\r
-        paginator = Paginator(models.User.active_instructors(), count)\r
-    elif action == 'drop':\r
-        paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter?\r
-    else:\r
-        paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter?\r
-        \r
-    return g.render('instructors.xhtml', paginator=paginator,\r
-                    page_num=page_num,\r
-                    count=count)\r
-\r
-def instructor_search(request, instructor):\r
-    return search(request, with_instructor=instructor)\r
-\r
-# MARK: propose we get rid of this. We have browse.\r
-def departments(request):\r
-    raise NotImplementedError\r
-\r
-\r
-def user_prefs(request):\r
-    if request.method != 'POST':\r
-        return g.render('prefs.xhtml')\r
-    else:\r
-        profile = request.user.get_profile()\r
-        profile.wants_email_notices = bool(request.POST.get('wants_email_notices'))\r
-        profile.save()\r
-        return HttpResponseRedirect('../')\r
-\r
-def z3950_test(request):\r
-    #z39.50 testing area\r
-\r
-\r
-    styledoc = libxml2.parseFile(HERE('../../static/xslt/test.xsl'))\r
-    stylexsl = libxslt.parseStylesheetDoc(styledoc)\r
-\r
-    #testing JZKitZ3950 - it seems to work, but i have a character set problem\r
-    #with the returned marc\r
-    #nope - the problem is weak mapping with the limited solr test set\r
-    #i think this can be sorted out\r
-\r
-    #conn = zoom.Connection ('z3950.loc.gov', 7090)\r
-    conn = zoom.Connection ('zed.concat.ca', 210)\r
-    print("connecting...")\r
-    conn.databaseName = 'OWA'\r
-    conn.preferredRecordSyntax = 'XML'\r
-    # conn.preferredRecordSyntax = 'USMARC'\r
-    query = zoom.Query ('CCL', 'ti="agar"')\r
-    res = conn.search (query)\r
-    collector = []\r
-    # if we wanted to get into funkiness\r
-    m = zmarc.MARC8_to_Unicode ()\r
-    for r in res:\r
-        #print(type(r.data))\r
-        #print(type(m.translate(r.data)))\r
-       zhit = str("<?xml version=\"1.0\"?>") + (m.translate(r.data))\r
-       #doc = libxml2.parseDoc(zhit)\r
-       #print(stylexsl.applyStylesheet(doc, None))\r
-\r
-    conn.close ()\r
-    res_str = "" . join(collector)\r
-    return g.render('z3950_test.xhtml', res_str=res_str)\r
-\r
-def browse(request, browse_option=''):\r
-    #the defaults should be moved into a config file or something...\r
-    page_num = int(request.GET.get('page', 1))\r
-    count    = int(request.GET.get('count', 5))\r
-\r
-    if browse_option == '':\r
-        queryset = None\r
-        template = 'browse_index.xhtml'\r
-    elif browse_option == 'instructors':\r
-        queryset = models.User.active_instructors()\r
-        queryset = queryset.filter(user_filters(request.user)['instructors'])\r
-        template = 'instructors.xhtml'\r
-    elif browse_option == 'departments':\r
-        queryset = models.Department.objects.filter(active=True)\r
-        template = 'departments.xhtml'\r
-    elif browse_option == 'courses':\r
-        # fixme, course filter should not be (active=True) but based on user identity.\r
-        for_courses = user_filters(request.user)['courses']\r
-        queryset = models.Course.objects.filter(for_courses)\r
-        template = 'courses.xhtml'\r
-\r
-    queryset = queryset and queryset.distinct()\r
-    paginator = Paginator(queryset, count)\r
-    return g.render(template, paginator=paginator,\r
-                    page_num=page_num,\r
-                    count=count)\r
-\r
-@login_required\r
-def my_courses(request):\r
-    return g.render('my_courses.xhtml')\r
-\r
-def instructor_detail(request, instructor_id):\r
-    page_num = int(request.GET.get('page', 1))\r
-    count = int(request.GET.get('count', 5))\r
-    '''\r
-    i am not sure this is the best way to go from instructor\r
-    to course\r
-    '''\r
-    courses = models.Course.objects.filter(member__user=instructor_id,\r
-                                           member__role='INSTR')\r
-    filters = user_filters(request.user)\r
-    courses = courses.filter(filters['courses'])\r
-    paginator = Paginator(courses.order_by('title'), count)\r
-\r
-    '''\r
-    no concept of active right now, maybe suppressed is a better\r
-    description anyway?\r
-    '''\r
-        # filter(active=True).order_by('title'), count)\r
-    instructor = models.User.objects.get(pk=instructor_id)\r
-    return g.render('courses.xhtml', \r
-                    custom_title=_('Courses taught by %s') % instructor.get_full_name(),\r
-                    paginator=paginator,\r
-                    page_num=page_num,\r
-                    count=count)\r
-\r
-def department_detail(request, department_id):\r
-    page_num = int(request.GET.get('page', 1))\r
-    count = int(request.GET.get('count', 5))\r
-\r
-    paginator = Paginator(models.Course.objects.\r
-        filter(department__id=department_id).\r
-        order_by('title'), count)\r
-\r
-    department = models.Department.objects.get(pk=department_id)\r
-\r
-    return g.render('courses.xhtml', \r
-            custom_title=_('Courses with Materials in %s') % department.name,\r
-            paginator=paginator,\r
-            page_num=page_num,\r
-            count=count)\r
-\r
+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)
+
+@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)
+
index eac0a21..0533213 100644 (file)
-from _common import *\r
-from django.utils.translation import ugettext as _\r
-\r
-@members_only\r
-def item_detail(request, course_id, item_id):\r
-    """Display an item (however that makes sense).""" \r
-    # really, displaying an item will vary based on what type of item\r
-    # it is -- e.g. a URL item would redirect to the target URL. I'd\r
-    # like this URL to be the generic dispatcher, but for now let's\r
-    # just display some metadata about the item.\r
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)\r
-    if item.url:\r
-        return _heading_url(request, item)\r
-    else:\r
-        return item_metadata(request, course_id, item_id)\r
-\r
-@members_only\r
-def item_metadata(request, course_id, item_id):\r
-    """Display a metadata page for the item."""\r
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)\r
-    if item.item_type == 'HEADING':\r
-        return _heading_detail(request, item)\r
-    else:\r
-        return g.render('item/item_metadata.xhtml', course=item.course,\r
-                        item=item)\r
-\r
-def _heading_url(request, item):\r
-    return HttpResponseRedirect(item.url)\r
-\r
-def _heading_detail(request, item):\r
-    """Display a heading. Show the subitems for this heading."""\r
-    return g.render('item/item_heading_detail.xhtml', item=item)\r
-\r
-\r
-@instructors_only\r
-def item_add(request, course_id, item_id):\r
-    # The parent_item_id is the id for the parent-heading item. Zero\r
-    # represents 'top-level', i.e. the new item should have no\r
-    # heading. \r
-    #For any other number, we must check that the parent\r
-    # item is of the Heading type.\r
-    parent_item_id = item_id\r
-    if parent_item_id=='0':\r
-        parent_item = None\r
-        course = get_object_or_404(models.Course, pk=course_id)\r
-        siblings = course.item_set.filter(parent_heading=None)\r
-    else:\r
-        parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)\r
-        assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')\r
-        course = parent_item.course\r
-        siblings = course.item_set.filter(parent_heading=parent_item)\r
-\r
-    try:\r
-        next_order = 1 + max(i.sort_order for i in siblings)\r
-    except:\r
-        next_order = 0\r
-    if not course.can_edit(request.user):\r
-        return _access_denied(_('You are not an editor.'))\r
-\r
-    item_type = request.GET.get('item_type')\r
-    assert item_type, _('No item_type parameter was provided.')\r
-\r
-    # for the moment, only HEADINGs, URLs and ELECs can be added. fixme.\r
-    assert item_type in ('HEADING', 'URL', 'ELEC', 'PHYS'), \\r
-        _('Sorry, only HEADINGs, URLs and ELECs can be added right now.')\r
-\r
-    if request.method != 'POST' and item_type == 'PHYS':\r
-        # special handling: send to catalogue search\r
-        return HttpResponseRedirect('cat_search/')\r
-\r
-    if request.method != 'POST':\r
-        item = models.Item()    # dummy object\r
-        metadata_formset = metadata_formset_class(queryset=item.metadata_set.all())\r
-        return g.render('item/item_add_%s.xhtml' % item_type.lower(),\r
-                        **locals())\r
-    else:\r
-        # fixme, this will need refactoring. But not yet.\r
-        author = request.user.get_full_name() or request.user.username\r
-        item = models.Item()    # dummy object\r
-        metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all())\r
-        assert metadata_formset.is_valid()\r
-        def do_metadata(item):\r
-            for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts\r
-                if not obj.get('DELETE'):\r
-                    item.metadata_set.create(name=obj['name'], value=obj['value'])\r
-            \r
-        if item_type == 'HEADING':\r
-            title = request.POST.get('title', '').strip()\r
-            if not title:\r
-                # fixme, better error handling.\r
-                return HttpResponseRedirect(request.get_full_path())\r
-            else:\r
-                item = models.Item(\r
-                    course=course,\r
-                    item_type='HEADING',\r
-                    sort_order = next_order,\r
-                    parent_heading=parent_item,\r
-                    title=title,\r
-                    )\r
-                item.save()\r
-                do_metadata(item)\r
-                item.save()\r
-        elif item_type == 'URL':\r
-            title = request.POST.get('title', '').strip()\r
-            url = request.POST.get('url', '').strip()\r
-            if not (title and url):\r
-                # fixme, better error handling.\r
-                return HttpResponseRedirect(request.get_full_path())\r
-            else:\r
-                item = models.Item(\r
-                    course=course,\r
-                    item_type='URL',\r
-                    parent_heading=parent_item,\r
-                    sort_order = next_order,\r
-                    title=title,\r
-                    url = url)\r
-                item.save()\r
-                do_metadata(item)\r
-                item.save()\r
-        elif item_type == 'ELEC':\r
-            title = request.POST.get('title', '').strip()\r
-            upload = request.FILES.get('file')\r
-            if not (title and upload):\r
-                # fixme, better error handling.\r
-                return HttpResponseRedirect(request.get_full_path())\r
-            item = models.Item(\r
-                course=course,\r
-                item_type='ELEC',\r
-                parent_heading=parent_item,\r
-                sort_order = next_order,\r
-                title=title,\r
-                fileobj_mimetype = upload.content_type,\r
-                )\r
-            item.fileobj.save(upload.name, upload)\r
-            item.save()\r
-            do_metadata(item)\r
-            item.save()\r
-        else:\r
-            raise NotImplementedError\r
-\r
-        if parent_item:\r
-            return HttpResponseRedirect(parent_item.item_url('meta'))\r
-        else:\r
-            return HttpResponseRedirect(course.course_url())\r
-\r
-@instructors_only\r
-def item_add_cat_search(request, course_id, item_id):\r
-    # this chunk stolen from item_add(). Refactor.\r
-    parent_item_id = item_id\r
-    if parent_item_id=='0':\r
-        parent_item = None\r
-        course = get_object_or_404(models.Course, pk=course_id)\r
-        siblings = course.item_set.filter(parent_heading=None)\r
-    else:\r
-        parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)\r
-        assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')\r
-        course = parent_item.course\r
-        siblings = course.item_set.filter(parent_heading=parent_item)\r
-\r
-    try:\r
-        next_order = 1 + max(i.sort_order for i in siblings)\r
-    except:\r
-        next_order = 0\r
-\r
-    #----------\r
-\r
-    if request.method != 'POST':\r
-        if not 'query' in request.GET:\r
-            return g.render('item/item_add_cat_search.xhtml', results=[], query='', \r
-                            course=course, parent_item=parent_item)\r
-        query = request.GET.get('query','').strip()\r
-        start, limit = (int(request.GET.get(k,v)) for k,v in (('start',1),('limit',10)))\r
-        results, numhits = lib_integration.cat_search(query, start, limit)\r
-        return g.render('item/item_add_cat_search.xhtml', \r
-                        results=results, query=query, \r
-                        start=start, limit=limit, numhits=numhits,\r
-                        course=course, parent_item=parent_item)\r
-    else:\r
-        # User has selected an item; add it to course site.\r
-        raw_pickitem = request.POST.get('pickitem', '').strip()\r
-        #fixme, this block copied from item_add. refactor.\r
-        parent_item_id = item_id\r
-        if parent_item_id == '0': \r
-            # no heading (toplevel)\r
-            parent_item = None\r
-            course = get_object_or_404(models.Course, pk=course_id)\r
-        else:\r
-            parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)\r
-            assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')\r
-            course = parent_item.course\r
-        if not course.can_edit(request.user):\r
-            return _access_denied(_('You are not an editor.'))\r
-\r
-        pickitem = simplejson.loads(raw_pickitem)\r
-        dublin = marcxml_dictionary_to_dc(pickitem)\r
-\r
-        # one last thing. If this picked item has an 856$9 field, then\r
-        # it's an electronic resource, not a physical item. In that\r
-        # case, we add it as a URL, not a PHYS.\r
-        if '8569' in pickitem:\r
-            dct = dict(item_type='URL', url=pickitem.get('856u'))\r
-        else:\r
-            dct = dict(item_type='PHYS')\r
-\r
-        item = course.item_set.create(parent_heading=parent_item,\r
-                                      sort_order=next_order,\r
-                                      title=dublin.get('dc:title','Untitled'),\r
-                                      **dct)\r
-        item.save()\r
-\r
-        for dc, value in dublin.items():\r
-            md = item.metadata_set.create(item=item, name=dc, value=value)\r
-        # store the whole darn MARC-dict as well (JSON)\r
-        item.metadata_set.create(item=item, name='syrup:marc', value=raw_pickitem)\r
-        item.save()\r
-        return HttpResponseRedirect('../../../%d/meta' % item.id)\r
-\r
-#------------------------------------------------------------\r
-\r
-#this is used in item_edit.\r
-metadata_formset_class = modelformset_factory(models.Metadata, \r
-                                              fields=['name','value'], \r
-                                              extra=3, can_delete=True)\r
-\r
-@instructors_only\r
-def item_edit(request, course_id, item_id):\r
-    course = get_object_or_404(models.Course, pk=course_id)\r
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)\r
-    item_type = item.item_type\r
-    template = 'item/item_add_%s.xhtml' % item_type.lower()\r
-    parent_item = item.parent_heading\r
-\r
-    if request.method != 'POST':\r
-        metadata_formset = metadata_formset_class(queryset=item.metadata_set.all())\r
-        return g.render(template, **locals())\r
-    else:\r
-        metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all())\r
-        assert metadata_formset.is_valid()\r
-        if 'file' in request.FILES:\r
-            # this is a 'replace-current-file' action.\r
-            upload = request.FILES.get('file')\r
-            item.fileobj.save(upload.name, upload)\r
-            item.fileobj_mimetype = upload.content_type\r
-        else:\r
-            # generally update the item.\r
-            [setattr(item, k, v) for (k,v) in request.POST.items()]\r
-            # generally update the metadata\r
-            item.metadata_set.all().delete()\r
-            for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts\r
-                if not obj.get('DELETE'):\r
-                    item.metadata_set.create(name=obj['name'], value=obj['value'])\r
-                    \r
-        item.save()\r
-        return HttpResponseRedirect(item.parent_url())\r
-        \r
-@instructors_only\r
-def item_delete(request, course_id, item_id):\r
-    course = get_object_or_404(models.Course, pk=course_id)\r
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)\r
-    if request.method != 'POST':\r
-        return g.render('item/item_delete_confirm.xhtml', **locals())\r
-    else:\r
-        if 'yes' in request.POST:\r
-            # I think Django's ON DELETE CASCADE-like behaviour will\r
-            # take care of the sub-items.\r
-            if item.parent_heading:\r
-                redir = HttpResponseRedirect(item.parent_heading.item_url('meta'))\r
-            else:\r
-                redir = HttpResponseRedirect(course.course_url())\r
-            item.delete()\r
-            return redir\r
-        else:\r
-            return HttpResponseRedirect('../meta')\r
-    \r
-@members_only\r
-def item_download(request, course_id, item_id, filename):\r
-    course = get_object_or_404(models.Course, pk=course_id)\r
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)\r
-    assert item.item_type == 'ELEC', _('Can only download ELEC documents!')\r
-    fileiter = item.fileobj.chunks()\r
-    resp = HttpResponse(fileiter)\r
-    resp['Content-Type'] = item.fileobj_mimetype or 'application/octet-stream'\r
-    #resp['Content-Disposition'] = 'attachment; filename=%s' % name\r
-    return resp\r
-    \r
-\r
-\r
-#------------------------------------------------------------\r
-# resequencing items\r
-\r
-def _reseq(request, course, parent_heading):\r
-    new_order = request.POST['new_order'].strip().split(' ')\r
-    # new_order is now a list like this: ['item_3', 'item_8', 'item_1', ...].\r
-    # get at the ints.\r
-    new_order = [int(n.split('_')[1]) for n in new_order]\r
-    print >> sys.stderr, new_order\r
-    the_items = list(course.item_set.filter(parent_heading=parent_heading).order_by('sort_order'))\r
-    # sort the items by position in new_order\r
-    the_items.sort(key=lambda item: new_order.index(item.id))\r
-    for newnum, item in enumerate(the_items):\r
-        item.sort_order = newnum\r
-        item.save()\r
-    return HttpResponse("'ok'");\r
-\r
-@instructors_only\r
-def course_reseq(request, course_id):\r
-    course = get_object_or_404(models.Course, pk=course_id)\r
-    parent_heading = None\r
-    return _reseq(request, course, parent_heading)\r
-\r
-@instructors_only\r
-def item_heading_reseq(request, course_id, item_id):\r
-    course = get_object_or_404(models.Course, pk=course_id)\r
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)\r
-    parent_heading = item\r
-    return _reseq(request, course, parent_heading)\r
-\r
-\r
-@instructors_only\r
-def item_relocate(request, course_id, item_id):\r
-    """Move an item from its current subheading to another one."""\r
-    course = get_object_or_404(models.Course, pk=course_id)\r
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)\r
-    if request.method != 'POST':\r
-        return g.render('item/item_relocate.xhtml', **locals())\r
-    else:\r
-        newheading = int(request.POST['heading'])\r
-        if newheading == 0:\r
-            new_parent = None\r
-        else:\r
-            new_parent = course.item_set.get(pk=newheading)\r
-            if item in new_parent.hierarchy():\r
-                # then we would create a cycle. Bail out.\r
-                return simple_message(_('Impossible item-move!'), \r
-                                      _('You cannot make an item a descendant of itself!'))\r
-        item.parent_heading = new_parent\r
-        item.save()\r
-        if new_parent:\r
-            return HttpResponseRedirect(new_parent.item_url('meta'))\r
-        else:\r
-            return HttpResponseRedirect(course.course_url())\r
-        \r
-        \r
-\r
-#-----------------------------------------------------------------------------\r
-# Physical item processing\r
-\r
-@admin_only                     # fixme, is this the right permission?\r
-def phys_index(request):\r
-    return g.render('phys/index.xhtml')\r
-\r
-@admin_only                     # fixme, is this the right permission?\r
-def phys_checkout(request):\r
-    if request.method != 'POST':\r
-        return g.render('phys/checkout.xhtml', step=1)\r
-    else:\r
-        post = lambda k: request.POST.get(k, '').strip()\r
-        # dispatch based on what 'step' we are at.\r
-        step = post('step')     \r
-        func = {'1': _phys_checkout_get_patron,\r
-                '2':_phys_checkout_do_checkout,\r
-                '3':_phys_checkout_do_another,\r
-                }[step]\r
-        return func(request)\r
-\r
-def _phys_checkout_get_patron(request):\r
-    post           = lambda k: request.POST.get(k, '').strip()\r
-    patron, item   = post('patron'), post('item')\r
-    msg            = lib_integration.patron_info(patron)\r
-    if not msg['success']:\r
-        return simple_message(_('Invalid patron barcode'),\r
-                              _('No such patron could be found.'))\r
-    else:\r
-        patron_descrip = '%s (%s) &mdash; %s' % (\r
-            msg['personal'], msg['home_library'], msg['screenmsg'])\r
-        return g.render('phys/checkout.xhtml', step=2, \r
-                        patron=patron, patron_descrip=patron_descrip)\r
-\r
-def _phys_checkout_do_checkout(request):\r
-    post           = lambda k: request.POST.get(k, '').strip()\r
-    patron, item   = post('patron'), post('item')\r
-    patron_descrip = post('patron_descrip')\r
-\r
-    # make sure the barcode actually matches with a known barcode in\r
-    # Syrup. We only checkout what we know about.\r
-    matches = models.Item.with_barcode(item)\r
-    if not matches:\r
-        is_successful = False\r
-        item_descrip  = None\r
-    else:\r
-        msg_status   = lib_integration.item_status(item)\r
-        msg_checkout = lib_integration.checkout(patron, item)\r
-        is_successful = msg_checkout['success']\r
-        item_descrip = '%s &mdash; %s' % (\r
-            msg_status['title'], msg_status['status'])\r
-\r
-    # log the checkout attempt.\r
-    log_entry = models.CheckInOut.objects.create(\r
-        is_checkout = True,\r
-        is_successful = is_successful,\r
-        staff = request.user,\r
-        patron = patron,\r
-        patron_descrip = patron_descrip,\r
-        item = item,\r
-        item_descrip = item_descrip)\r
-    log_entry.save()\r
-\r
-    if not matches:\r
-        return simple_message(\r
-            _('Item not found in Reserves'),\r
-            _('This item does not exist in the Reserves database! '\r
-              'Cannot check it out.'))\r
-    else:\r
-        return g.render('phys/checkout.xhtml', step=3, \r
-                        patron=patron, item=item,\r
-                        patron_descrip=patron_descrip,\r
-                        checkout_result=msg_checkout,\r
-                        item_descrip=item_descrip)\r
-\r
-def _phys_checkout_do_another(request):\r
-    post           = lambda k: request.POST.get(k, '').strip()\r
-    patron         = post('patron')\r
-    patron_descrip = post('patron_descrip')\r
-    return g.render('phys/checkout.xhtml', step=2, \r
-                    patron=patron,\r
-                    patron_descrip=patron_descrip)\r
-\r
-#------------------------------------------------------------\r
-\r
-@admin_only        \r
-def phys_mark_arrived(request):\r
-    if request.method != 'POST':\r
-        return g.render('phys/mark_arrived.xhtml')\r
-    else:\r
-        barcode = request.POST.get('item', '').strip()\r
-        already = models.PhysicalObject.by_barcode(barcode)\r
-        if already:\r
-            msg = _('This item has already been marked as received. Date received: %s')\r
-            msg = msg % str(already.received)\r
-            return simple_message(_('Item already marked as received'), msg)\r
-        bib_id  = lib_integration.barcode_to_bib_id(barcode)\r
-        if not bib_id:\r
-            return simple_message(_('Item not found'), \r
-                                  _('No item matching this barcode could be found.'))\r
-\r
-        marcxml = lib_integration.bib_id_to_marcxml(bib_id)\r
-        dct     = marcxml_to_dictionary(marcxml)\r
-        dublin  = marcxml_dictionary_to_dc(dct)\r
-        # merge them\r
-        dct.update(dublin)\r
-        ranked = rank_pending_items(dct)\r
-        return g.render('phys/mark_arrived_choose.xhtml', \r
-                        barcode=barcode,\r
-                        bib_id=bib_id,\r
-                        ranked=ranked,\r
-                        metadata=dct)\r
-\r
-@admin_only        \r
-def phys_mark_arrived_match(request):\r
-    choices = [int(k.split('_')[1]) for k in request.POST if k.startswith('choose_')]\r
-    if not choices:\r
-        return simple_message(_('No matching items selected!'),\r
-                              _('You must select one or more matching items from the list.'))\r
-    else:\r
-        barcode = request.POST.get('barcode', '').strip()\r
-        assert barcode\r
-        smallint = request.POST.get('smallint', '').strip() or None\r
-        try:\r
-            phys = models.PhysicalObject(barcode=barcode,\r
-                                         receiver = request.user,\r
-                                         smallint = smallint)\r
-            phys.save()\r
-        except Exception, e:\r
-            return simple_message(_('Error'), repr(e), go_back=True)\r
-\r
-        for c in choices:\r
-            item = models.Item.objects.get(pk=c)\r
-            current_bc = item.barcode()\r
-            if current_bc:\r
-                item.metadata_set.filter(name='syrup:barcode').delete()\r
-            item.metadata_set.create(name='syrup:barcode', value=barcode)\r
-            item.save()\r
-    return g.render('phys/mark_arrived_outcome.xhtml')\r
-\r
-@admin_only\r
-def phys_circlist(request):\r
-    term_code = request.GET.get('term')\r
-    if not term_code:\r
-        terms = models.Term.objects.order_by('code')\r
-        return g.render('phys/circlist_index.xhtml', terms=terms)\r
-\r
-    term = get_object_or_404(models.Term, code=term_code)\r
-\r
-    # gather the list of wanted items for this term.\r
-    # Fixme, I need a better way.\r
-\r
-    cursor = django.db.connection.cursor()\r
-    q = "select item_id from syrup_metadata where name='syrup:barcode'"\r
-    cursor.execute(q)\r
-    bad_ids = set([r[0] for r in cursor.fetchall()])\r
-    cursor.close()\r
-\r
-    wanted = models.Item.objects.filter(\r
-        item_type='PHYS', course__term=term).select_related('metadata')\r
-    wanted = [w for w in wanted if w.id not in bad_ids]\r
-    return g.render('phys/circlist_for_term.xhtml', \r
-                    term=term,\r
-                    wanted=wanted)\r
-    \r
-\r
+from _common import *
+from django.utils.translation import ugettext as _
+
+@members_only
+def item_detail(request, course_id, item_id):
+    """Display an item (however that makes sense).""" 
+    # really, displaying an item will vary based on what type of item
+    # it is -- e.g. a URL item would redirect to the target URL. I'd
+    # like this URL to be the generic dispatcher, but for now let's
+    # just display some metadata about the item.
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    if item.url:
+        return _heading_url(request, item)
+    else:
+        return item_metadata(request, course_id, item_id)
+
+@members_only
+def item_metadata(request, course_id, item_id):
+    """Display a metadata page for the item."""
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    if item.item_type == 'HEADING':
+        return _heading_detail(request, item)
+    else:
+        return g.render('item/item_metadata.xhtml', course=item.course,
+                        item=item)
+
+def _heading_url(request, item):
+    return HttpResponseRedirect(item.url)
+
+def _heading_detail(request, item):
+    """Display a heading. Show the subitems for this heading."""
+    return g.render('item/item_heading_detail.xhtml', item=item)
+
+
+@instructors_only
+def item_add(request, course_id, item_id):
+    # The parent_item_id is the id for the parent-heading item. Zero
+    # represents 'top-level', i.e. the new item should have no
+    # heading. 
+    #For any other number, we must check that the parent
+    # item is of the Heading type.
+    parent_item_id = item_id
+    if parent_item_id=='0':
+        parent_item = None
+        course = get_object_or_404(models.Course, pk=course_id)
+        siblings = course.item_set.filter(parent_heading=None)
+    else:
+        parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)
+        assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')
+        course = parent_item.course
+        siblings = course.item_set.filter(parent_heading=parent_item)
+
+    try:
+        next_order = 1 + max(i.sort_order for i in siblings)
+    except:
+        next_order = 0
+    if not course.can_edit(request.user):
+        return _access_denied(_('You are not an editor.'))
+
+    item_type = request.GET.get('item_type')
+    assert item_type, _('No item_type parameter was provided.')
+
+    # for the moment, only HEADINGs, URLs and ELECs can be added. fixme.
+    assert item_type in ('HEADING', 'URL', 'ELEC', 'PHYS'), \
+        _('Sorry, only HEADINGs, URLs and ELECs can be added right now.')
+
+    if request.method != 'POST' and item_type == 'PHYS':
+        # special handling: send to catalogue search
+        return HttpResponseRedirect('cat_search/')
+
+    if request.method != 'POST':
+        item = models.Item()    # dummy object
+        metadata_formset = metadata_formset_class(queryset=item.metadata_set.all())
+        return g.render('item/item_add_%s.xhtml' % item_type.lower(),
+                        **locals())
+    else:
+        # fixme, this will need refactoring. But not yet.
+        author = request.user.get_full_name() or request.user.username
+        item = models.Item()    # dummy object
+        metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all())
+        assert metadata_formset.is_valid()
+        def do_metadata(item):
+            for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts
+                if not obj.get('DELETE'):
+                    item.metadata_set.create(name=obj['name'], value=obj['value'])
+            
+        if item_type == 'HEADING':
+            title = request.POST.get('title', '').strip()
+            if not title:
+                # fixme, better error handling.
+                return HttpResponseRedirect(request.get_full_path())
+            else:
+                item = models.Item(
+                    course=course,
+                    item_type='HEADING',
+                    sort_order = next_order,
+                    parent_heading=parent_item,
+                    title=title,
+                    )
+                item.save()
+                do_metadata(item)
+                item.save()
+        elif item_type == 'URL':
+            title = request.POST.get('title', '').strip()
+            url = request.POST.get('url', '').strip()
+            if not (title and url):
+                # fixme, better error handling.
+                return HttpResponseRedirect(request.get_full_path())
+            else:
+                item = models.Item(
+                    course=course,
+                    item_type='URL',
+                    parent_heading=parent_item,
+                    sort_order = next_order,
+                    title=title,
+                    url = url)
+                item.save()
+                do_metadata(item)
+                item.save()
+        elif item_type == 'ELEC':
+            title = request.POST.get('title', '').strip()
+            upload = request.FILES.get('file')
+            if not (title and upload):
+                # fixme, better error handling.
+                return HttpResponseRedirect(request.get_full_path())
+            item = models.Item(
+                course=course,
+                item_type='ELEC',
+                parent_heading=parent_item,
+                sort_order = next_order,
+                title=title,
+                fileobj_mimetype = upload.content_type,
+                )
+            item.fileobj.save(upload.name, upload)
+            item.save()
+            do_metadata(item)
+            item.save()
+        else:
+            raise NotImplementedError
+
+        if parent_item:
+            return HttpResponseRedirect(parent_item.item_url('meta'))
+        else:
+            return HttpResponseRedirect(course.course_url())
+
+@instructors_only
+def item_add_cat_search(request, course_id, item_id):
+    # this chunk stolen from item_add(). Refactor.
+    parent_item_id = item_id
+    if parent_item_id=='0':
+        parent_item = None
+        course = get_object_or_404(models.Course, pk=course_id)
+        siblings = course.item_set.filter(parent_heading=None)
+    else:
+        parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)
+        assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')
+        course = parent_item.course
+        siblings = course.item_set.filter(parent_heading=parent_item)
+
+    try:
+        next_order = 1 + max(i.sort_order for i in siblings)
+    except:
+        next_order = 0
+
+    #----------
+
+    if request.method != 'POST':
+        if not 'query' in request.GET:
+            return g.render('item/item_add_cat_search.xhtml', results=[], query='', 
+                            course=course, parent_item=parent_item)
+        query = request.GET.get('query','').strip()
+        start, limit = (int(request.GET.get(k,v)) for k,v in (('start',1),('limit',10)))
+        results, numhits = lib_integration.cat_search(query, start, limit)
+        return g.render('item/item_add_cat_search.xhtml', 
+                        results=results, query=query, 
+                        start=start, limit=limit, numhits=numhits,
+                        course=course, parent_item=parent_item)
+    else:
+        # User has selected an item; add it to course site.
+        raw_pickitem = request.POST.get('pickitem', '').strip()
+        #fixme, this block copied from item_add. refactor.
+        parent_item_id = item_id
+        if parent_item_id == '0': 
+            # no heading (toplevel)
+            parent_item = None
+            course = get_object_or_404(models.Course, pk=course_id)
+        else:
+            parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)
+            assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')
+            course = parent_item.course
+        if not course.can_edit(request.user):
+            return _access_denied(_('You are not an editor.'))
+
+        pickitem = simplejson.loads(raw_pickitem)
+        dublin = marcxml_dictionary_to_dc(pickitem)
+
+        # one last thing. If this picked item has an 856$9 field, then
+        # it's an electronic resource, not a physical item. In that
+        # case, we add it as a URL, not a PHYS.
+        if '8569' in pickitem:
+            dct = dict(item_type='URL', url=pickitem.get('856u'))
+        else:
+            dct = dict(item_type='PHYS')
+
+        item = course.item_set.create(parent_heading=parent_item,
+                                      sort_order=next_order,
+                                      title=dublin.get('dc:title','Untitled'),
+                                      **dct)
+        item.save()
+
+        for dc, value in dublin.items():
+            md = item.metadata_set.create(item=item, name=dc, value=value)
+        # store the whole darn MARC-dict as well (JSON)
+        item.metadata_set.create(item=item, name='syrup:marc', value=raw_pickitem)
+        item.save()
+        return HttpResponseRedirect('../../../%d/meta' % item.id)
+
+#------------------------------------------------------------
+
+#this is used in item_edit.
+metadata_formset_class = modelformset_factory(models.Metadata, 
+                                              fields=['name','value'], 
+                                              extra=3, can_delete=True)
+
+@instructors_only
+def item_edit(request, course_id, item_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    item_type = item.item_type
+    template = 'item/item_add_%s.xhtml' % item_type.lower()
+    parent_item = item.parent_heading
+
+    if request.method != 'POST':
+        metadata_formset = metadata_formset_class(queryset=item.metadata_set.all())
+        return g.render(template, **locals())
+    else:
+        metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all())
+        assert metadata_formset.is_valid()
+        if 'file' in request.FILES:
+            # this is a 'replace-current-file' action.
+            upload = request.FILES.get('file')
+            item.fileobj.save(upload.name, upload)
+            item.fileobj_mimetype = upload.content_type
+        else:
+            # generally update the item.
+            [setattr(item, k, v) for (k,v) in request.POST.items()]
+            # generally update the metadata
+            item.metadata_set.all().delete()
+            for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts
+                if not obj.get('DELETE'):
+                    item.metadata_set.create(name=obj['name'], value=obj['value'])
+                    
+        item.save()
+        return HttpResponseRedirect(item.parent_url())
+        
+@instructors_only
+def item_delete(request, course_id, item_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    if request.method != 'POST':
+        return g.render('item/item_delete_confirm.xhtml', **locals())
+    else:
+        if 'yes' in request.POST:
+            # I think Django's ON DELETE CASCADE-like behaviour will
+            # take care of the sub-items.
+            if item.parent_heading:
+                redir = HttpResponseRedirect(item.parent_heading.item_url('meta'))
+            else:
+                redir = HttpResponseRedirect(course.course_url())
+            item.delete()
+            return redir
+        else:
+            return HttpResponseRedirect('../meta')
+    
+@members_only
+def item_download(request, course_id, item_id, filename):
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    assert item.item_type == 'ELEC', _('Can only download ELEC documents!')
+    fileiter = item.fileobj.chunks()
+    resp = HttpResponse(fileiter)
+    resp['Content-Type'] = item.fileobj_mimetype or 'application/octet-stream'
+    #resp['Content-Disposition'] = 'attachment; filename=%s' % name
+    return resp
+    
+
+
+#------------------------------------------------------------
+# resequencing items
+
+def _reseq(request, course, parent_heading):
+    new_order = request.POST['new_order'].strip().split(' ')
+    # new_order is now a list like this: ['item_3', 'item_8', 'item_1', ...].
+    # get at the ints.
+    new_order = [int(n.split('_')[1]) for n in new_order]
+    print >> sys.stderr, new_order
+    the_items = list(course.item_set.filter(parent_heading=parent_heading).order_by('sort_order'))
+    # sort the items by position in new_order
+    the_items.sort(key=lambda item: new_order.index(item.id))
+    for newnum, item in enumerate(the_items):
+        item.sort_order = newnum
+        item.save()
+    return HttpResponse("'ok'");
+
+@instructors_only
+def course_reseq(request, course_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    parent_heading = None
+    return _reseq(request, course, parent_heading)
+
+@instructors_only
+def item_heading_reseq(request, course_id, item_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    parent_heading = item
+    return _reseq(request, course, parent_heading)
+
+
+@instructors_only
+def item_relocate(request, course_id, item_id):
+    """Move an item from its current subheading to another one."""
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    if request.method != 'POST':
+        return g.render('item/item_relocate.xhtml', **locals())
+    else:
+        newheading = int(request.POST['heading'])
+        if newheading == 0:
+            new_parent = None
+        else:
+            new_parent = course.item_set.get(pk=newheading)
+            if item in new_parent.hierarchy():
+                # then we would create a cycle. Bail out.
+                return simple_message(_('Impossible item-move!'), 
+                                      _('You cannot make an item a descendant of itself!'))
+        item.parent_heading = new_parent
+        item.save()
+        if new_parent:
+            return HttpResponseRedirect(new_parent.item_url('meta'))
+        else:
+            return HttpResponseRedirect(course.course_url())
+        
+        
+
+#-----------------------------------------------------------------------------
+# Physical item processing
+
+@admin_only                     # fixme, is this the right permission?
+def phys_index(request):
+    return g.render('phys/index.xhtml')
+
+@admin_only                     # fixme, is this the right permission?
+def phys_checkout(request):
+    if request.method != 'POST':
+        return g.render('phys/checkout.xhtml', step=1)
+    else:
+        post = lambda k: request.POST.get(k, '').strip()
+        # dispatch based on what 'step' we are at.
+        step = post('step')     
+        func = {'1': _phys_checkout_get_patron,
+                '2':_phys_checkout_do_checkout,
+                '3':_phys_checkout_do_another,
+                }[step]
+        return func(request)
+
+def _phys_checkout_get_patron(request):
+    post           = lambda k: request.POST.get(k, '').strip()
+    patron, item   = post('patron'), post('item')
+    msg            = lib_integration.patron_info(patron)
+    if not msg['success']:
+        return simple_message(_('Invalid patron barcode'),
+                              _('No such patron could be found.'))
+    else:
+        patron_descrip = '%s (%s) &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)
+
+#------------------------------------------------------------
+
+@admin_only        
+def phys_mark_arrived(request):
+    if request.method != 'POST':
+        return g.render('phys/mark_arrived.xhtml')
+    else:
+        barcode = request.POST.get('item', '').strip()
+        already = models.PhysicalObject.by_barcode(barcode)
+        if already:
+            msg = _('This item has already been marked as received. Date received: %s')
+            msg = msg % str(already.received)
+            return simple_message(_('Item already marked as received'), msg)
+        bib_id  = lib_integration.barcode_to_bib_id(barcode)
+        if not bib_id:
+            return simple_message(_('Item not found'), 
+                                  _('No item matching this barcode could be found.'))
+
+        marcxml = lib_integration.bib_id_to_marcxml(bib_id)
+        dct     = marcxml_to_dictionary(marcxml)
+        dublin  = marcxml_dictionary_to_dc(dct)
+        # merge them
+        dct.update(dublin)
+        ranked = rank_pending_items(dct)
+        return g.render('phys/mark_arrived_choose.xhtml', 
+                        barcode=barcode,
+                        bib_id=bib_id,
+                        ranked=ranked,
+                        metadata=dct)
+
+@admin_only        
+def phys_mark_arrived_match(request):
+    choices = [int(k.split('_')[1]) for k in request.POST if k.startswith('choose_')]
+    if not choices:
+        return simple_message(_('No matching items selected!'),
+                              _('You must select one or more matching items from the list.'))
+    else:
+        barcode = request.POST.get('barcode', '').strip()
+        assert barcode
+        smallint = request.POST.get('smallint', '').strip() or None
+        try:
+            phys = models.PhysicalObject(barcode=barcode,
+                                         receiver = request.user,
+                                         smallint = smallint)
+            phys.save()
+        except Exception, e:
+            return simple_message(_('Error'), repr(e), go_back=True)
+
+        for c in choices:
+            item = models.Item.objects.get(pk=c)
+            current_bc = item.barcode()
+            if current_bc:
+                item.metadata_set.filter(name='syrup:barcode').delete()
+            item.metadata_set.create(name='syrup:barcode', value=barcode)
+            item.save()
+    return g.render('phys/mark_arrived_outcome.xhtml')
+
+@admin_only
+def phys_circlist(request):
+    term_code = request.GET.get('term')
+    if not term_code:
+        terms = models.Term.objects.order_by('code')
+        return g.render('phys/circlist_index.xhtml', terms=terms)
+
+    term = get_object_or_404(models.Term, code=term_code)
+
+    # gather the list of wanted items for this term.
+    # Fixme, I need a better way.
+
+    cursor = django.db.connection.cursor()
+    q = "select item_id from syrup_metadata where name='syrup:barcode'"
+    cursor.execute(q)
+    bad_ids = set([r[0] for r in cursor.fetchall()])
+    cursor.close()
+
+    wanted = models.Item.objects.filter(
+        item_type='PHYS', course__term=term).select_related('metadata')
+    wanted = [w for w in wanted if w.id not in bad_ids]
+    return g.render('phys/circlist_for_term.xhtml', 
+                    term=term,
+                    wanted=wanted)
+    
+
index 5a3673a..c87711f 100644 (file)
@@ -1,26 +1,26 @@
-<?python\r
-title = defined('custom_title') and custom_title or _('Departments')\r
-?>\r
-<html xmlns="http://www.w3.org/1999/xhtml"\r
-      xmlns:xi="http://www.w3.org/2001/XInclude"\r
-      xmlns:py="http://genshi.edgewall.org/">\r
-<xi:include href="master.xhtml"/>\r
-<xi:include href="paginate.xhtml"/>\r
-<head>\r
-  <title>${title}</title>\r
-  <script type="text/javascript">\r
-    <!-- !This ought to be in paginate.xhtml, not here. how to do? -->\r
-    $(function() { $('.pagetable').tablesorter(); });\r
-  </script>\r
-</head>\r
-<body>\r
-  <h1>${title}</h1>\r
-  <tr py:def="pageheader()">\r
-    <th>Department</th>\r
-  </tr>\r
-  <span py:def="pagerow(department)">\r
-    <td><a href="${ROOT}${department_url(department)}">${department.name}</a></td>\r
-  </span>\r
-  ${pagetable(paginator, count, pagerow, pageheader)}\r
-</body>\r
-</html>\r
+<?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>
index 03dc2bf..8c6c1de 100644 (file)
@@ -1,58 +1,58 @@
-<?python\r
-if instance.id:\r
-    title = _('Edit course details')\r
-else:\r
-    title = _('Create a new course site')\r
-?>\r
-<html xmlns="http://www.w3.org/1999/xhtml"\r
-      xmlns:xi="http://www.w3.org/2001/XInclude"\r
-      xmlns:py="http://genshi.edgewall.org/">\r
-<xi:include href="master.xhtml"/>\r
-<xi:include href="components/course.xhtml"/>\r
-<head>\r
-  <title>${title}</title>\r
-  <!--\r
-  Disabling this title lookup for now, something goes amiss in the browser interaction\r
-  and the title field is disabled when it shouldn't be - art\r
-  -->\r
-  <!--\r
-  <script type="text/javascript" src="${ROOT}/static/edit_course.js"/>\r
-  -->\r
-</head>\r
-<body>\r
-  <div py:if="instance.id">${course_banner(instance)}</div>\r
-  <h1>${title}</h1>\r
-  <p py:if="instance.id"><a href="permission/">Edit course permissions</a> &bull; <a href="${instance.course_url()}">Return to course page</a></p>\r
-  <form action="." method="POST">\r
-    <tr py:def="field_row(field, example=None)">\r
-      <th>${field.label}</th>\r
-      <td>\r
-       <ul py:if="field.errors" class="errorlist">\r
-         <li py:for="err in field.errors">${err}</li>\r
-       </ul>\r
-       ${Markup(field)}\r
-      </td>\r
-      <td class="example" py:if="example">e.g., ${example}</td>\r
-    </tr>\r
-    <h2>General description</h2>\r
-    <table class="metadata_table">\r
-    ${field_row(form.code, example)}\r
-    ${field_row(form.title)}\r
-    ${field_row(form.term)}\r
-    ${field_row(form.department)}\r
-    <!-- <tr><th>Department</th><td>${Markup(form.department)} ${errorlist(form.department)}</td></tr> -->\r
-  </table>\r
-  <p><input type="submit" value="Continue"/> ${go_back_link()}</p>\r
-  </form>\r
-  <div class="gap"/>\r
-  <div py:if="instance.id">\r
-    <h2>Delete this course</h2>\r
-    <form action="delete/" method="POST">\r
-      <p><input type="checkbox" name="confirm_delete" id="confirm_delete"/>\r
-      <label for="confirm_delete">Yes, I want to delete this course site and all of its contents.</label>\r
-      </p>\r
-      <p><input type="submit" value="Delete this course"/> ${go_back_link()}</p>\r
-    </form>\r
-  </div>\r
-</body>\r
-</html>\r
+<?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>
index 90061e7..865ad4c 100644 (file)
@@ -1,89 +1,89 @@
-<?python\r
-from django.utils.simplejson import dumps\r
-from conifer.libsystems.z3950.marcxml import marcxml_dictionary_to_dc as to_dublin\r
-title = _('Add physical or electronic item, by catalogue search')\r
-helptext = _('Use keywords or CCL syntax for searching, for example: ti="detroit river" and au="wilgus"')\r
-dc_keys = ['dc:title', 'dc:creator', 'dc:publisher', 'dc:date']\r
-?>\r
-<html xmlns="http://www.w3.org/1999/xhtml"\r
-      xmlns:xi="http://www.w3.org/2001/XInclude"\r
-      xmlns:py="http://genshi.edgewall.org/">\r
-<xi:include href="../master.xhtml"/>\r
-<xi:include href="../paginate.xhtml"/>\r
-<xi:include href="../components/course.xhtml"/>\r
-<head>\r
-  <title>${title}</title>\r
-  <script type="text/javascript">\r
-    <!-- !This ought to be in paginate.xhtml, not here. how to do? -->\r
-    $(function() { $('.pagetable').tablesorter(); });\r
-  </script>\r
-  <script py:if="not 'query' in request.GET">                 <!-- !focus on query box if nothing to scroll. -->\r
-    $(function() { $('#query').focus(); });\r
-  </script>\r
-</head>\r
-<body>\r
-    ${course_banner(course)}\r
-    ${nested_title(parent_item)}\r
-    <h2>${title}</h2>\r
-    <div class="helptext">\r
-    ${helptext}\r
-    </div>\r
-\r
-    <form method="GET" action=".">\r
-      <input type="text" id="query" name="query" value="${query}" \r
-            style="font-size: larger; width: 600px;"/>\r
-      <input type="submit" value="Search"/>\r
-       ${go_back_link()}\r
-\r
-    </form>\r
-    <div py:def="page_control" py:if="results">\r
-      <p>\r
-       ${start}&ndash;${min(numhits, start+limit-1)} of ${numhits} results.\r
-       <span py:if="start-limit&gt;0">\r
-         <a href=".?query=${query}&amp;start=${start-limit}&amp;limit=${limit}">Previous ${limit}</a>\r
-         &bull;\r
-       </span>\r
-       <span py:if="start+limit&lt;numhits">\r
-         <a href=".?query=${query}&amp;start=${start+limit}&amp;limit=${limit}">Next ${limit}</a>\r
-       </span>\r
-      </p>\r
-    </div>\r
-    ${page_control()}\r
-    <table class="pagetable" py:if="'query' in request.GET">\r
-      <thead>\r
-       <tr><th>#</th><th>Title</th><th>Author</th><th>Publisher</th><th>PubDate</th></tr>\r
-      </thead>\r
-      <tbody py:for="resultnum, res in enumerate(results)"\r
-            py:with="dc=to_dublin(res)">\r
-       <tr>\r
-         <td>${resultnum+start}.</td>\r
-         <td>\r
-           ${dc.get('dc:title', '???')}\r
-           <a href="javascript:$('#full_${resultnum}').toggle(); void(0);">details</a>\r
-           <p py:if="res.get('8569')" style="margin: 8px 0; font-size: 90%; color: darkred;">\r
-             Electronic resource. <a href="${res.get('856u')}">view</a>\r
-           </p>\r
-         </td>\r
-         <td py:for="k in dc_keys[1:]">${dc.get(k) or '&mdash;'}</td>\r
-         <td>\r
-           <form action="." method="POST">\r
-             <input type="hidden" name="pickitem" value="${dumps(res)}"/>\r
-             <input type="submit" value="Pick this item"/>\r
-           </form>\r
-         </td>\r
-       </tr>\r
-       <tr id="full_${resultnum}" style="display: none;">\r
-         <td colspan="4" style="padding-left: 36;">\r
-           <table class="metadata_table">\r
-             <?python allkeys = res.keys(); allkeys.sort(); ?>\r
-             <tr py:for="k in allkeys">\r
-               <th>${k}</th><td>${res[k]}</td>\r
-             </tr>\r
-           </table>\r
-         </td>\r
-       </tr>\r
-      </tbody>\r
-    </table>\r
-    ${page_control()}\r
- </body>\r
-</html>\r
+<?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>
index ba1506d..7f36382 100644 (file)
@@ -1,79 +1,79 @@
-<?python\r
-app_name = _('Syrup Reserves System')\r
-search = _('search...')\r
-import os\r
-?>\r
-<html xmlns="http://www.w3.org/1999/xhtml"\r
-      xmlns:py="http://genshi.edgewall.org/"\r
-      xmlns:xi="http://www.w3.org/2001/XInclude"\r
-      py:strip="">\r
-  <py:match path="head" once="True">\r
-    <head py:attrs="select('@*')"\r
-         py:with="t=list(select('title/text()'))">\r
-      <title>${app_name}<py:if test="t">: ${t}</py:if></title>\r
-    <link rel="stylesheet" type="text/css" href="${ROOT}/static/main.css"/>\r
-    <!--\r
-    using the trunk version of jquery to get around some IE 8 problems\r
-    -->\r
-    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery.js"/>\r
-    <!--\r
-    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery-1.3.2.min.js"/>\r
-    -->\r
-    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery-ui-1.7.1.custom.min.js"/>\r
-    <script type="text/javascript" src="${ROOT}/static/jquery/js/jquery.tablesorter.min.js"/>\r
-    ${select('*[local-name()!="title"]')}\r
-    </head>\r
-  </py:match>\r
-  <py:match path="body" once="true">\r
-    <body py:attrs="select('@*')">\r
-      <div id="outer">\r
-      <div id="brandheader">\r
-       <div style="float: right; font-size: x-large; padding: 12px; color: #888;">\r
-         ${app_name}\r
-       </div>\r
-       <img src="${ROOT}/static/institution-logo.png" style="height: 50px;"/>\r
-      </div>\r
-        <!--\r
-      <div id="header" py:if="user.is_authenticated()">\r
-        trying to keep the search box consistent for now\r
-        -->\r
-      <div id="header">\r
-        <div id="search">\r
-            <form method="get" action="${ROOT}/search" \r
-                onsubmit="if(q.value.replace(/^\s*/, '').replace(/\s*$/, '') =='') return false;"\r
-            >\r
-            <input id="q" name="q" maxlength="100" size="25" type="text" \r
-                value="${search}" onblur="if(this.value=='') this.value='${search}';" \r
-                onfocus="if(this.value=='${search}') this.value='';"/>\r
-            </form>\r
-        </div>\r
-      <div id="welcome" py:if="user.is_authenticated()">\r
-       <strong style="padding-right: 18px;">Welcome, ${user.first_name or user.username}!</strong>\r
-       <a href="${ROOT}/accounts/logout">Log Out</a>\r
-       &bull; <a href="${ROOT}/prefs/">Preferences</a>\r
-      </div>\r
-      <div id="welcome" py:if="not user.is_authenticated()">\r
-       <strong style="padding-right: 18px;">Welcome!</strong>\r
-       <a class="loginbutton" href="${ROOT}/accounts/login/">Log In</a>\r
-       &bull; <a href="${ROOT}/prefs/">Preferences</a>\r
-      </div>\r
-    </div>\r
-      <xi:include py:if="user.is_authenticated()" href="tabbar.xhtml"/>\r
-      <xi:include py:if="not user.is_authenticated()" href="tabbar_anonymous.xhtml"/>\r
-      <div id="mainpanel">\r
-       ${select('*|text()')}\r
-      </div>\r
-      <div id="footer">\r
-       <div> \r
-    Syrup is a subproject of <a href="http://conifer.mcmaster.ca/">Project Conifer</a> &copy; 2009\r
-    </div>\r
-      </div>\r
-      </div>\r
-    </body>\r
-  </py:match>\r
-\r
-  <span py:def="go_back_link(url=None, msg=_('or Cancel'))" py:with="url=url or request.META.get('HTTP_REFERER', '../')">\r
-    <a style="margin-left: 12px;" href="${url}">${msg}</a>\r
-  </span>\r
-\r
-</html>\r
+<?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>