-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
-# 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]))
+
-# 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
-# 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'}))
+
+
-# 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))
-# 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:///'
-/*\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);
-/*\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
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
-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; }
+
-<?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
-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'),
+)
-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)
+
-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) — %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 — %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) — %s' % (
+ msg['personal'], msg['home_library'], msg['screenmsg'])
+ return g.render('phys/checkout.xhtml', step=2,
+ patron=patron, patron_descrip=patron_descrip)
+
+def _phys_checkout_do_checkout(request):
+ post = lambda k: request.POST.get(k, '').strip()
+ patron, item = post('patron'), post('item')
+ patron_descrip = post('patron_descrip')
+
+ # make sure the barcode actually matches with a known barcode in
+ # Syrup. We only checkout what we know about.
+ matches = models.Item.with_barcode(item)
+ if not matches:
+ is_successful = False
+ item_descrip = None
+ else:
+ msg_status = lib_integration.item_status(item)
+ msg_checkout = lib_integration.checkout(patron, item)
+ is_successful = msg_checkout['success']
+ item_descrip = '%s — %s' % (
+ msg_status['title'], msg_status['status'])
+
+ # log the checkout attempt.
+ log_entry = models.CheckInOut.objects.create(
+ is_checkout = True,
+ is_successful = is_successful,
+ staff = request.user,
+ patron = patron,
+ patron_descrip = patron_descrip,
+ item = item,
+ item_descrip = item_descrip)
+ log_entry.save()
+
+ if not matches:
+ return simple_message(
+ _('Item not found in Reserves'),
+ _('This item does not exist in the Reserves database! '
+ 'Cannot check it out.'))
+ else:
+ return g.render('phys/checkout.xhtml', step=3,
+ patron=patron, item=item,
+ patron_descrip=patron_descrip,
+ checkout_result=msg_checkout,
+ item_descrip=item_descrip)
+
+def _phys_checkout_do_another(request):
+ post = lambda k: request.POST.get(k, '').strip()
+ patron = post('patron')
+ patron_descrip = post('patron_descrip')
+ return g.render('phys/checkout.xhtml', step=2,
+ patron=patron,
+ patron_descrip=patron_descrip)
+
+#------------------------------------------------------------
+
+@admin_only
+def phys_mark_arrived(request):
+ if request.method != 'POST':
+ return g.render('phys/mark_arrived.xhtml')
+ else:
+ barcode = request.POST.get('item', '').strip()
+ already = models.PhysicalObject.by_barcode(barcode)
+ if already:
+ msg = _('This item has already been marked as received. Date received: %s')
+ msg = msg % str(already.received)
+ return simple_message(_('Item already marked as received'), msg)
+ bib_id = lib_integration.barcode_to_bib_id(barcode)
+ if not bib_id:
+ return simple_message(_('Item not found'),
+ _('No item matching this barcode could be found.'))
+
+ marcxml = lib_integration.bib_id_to_marcxml(bib_id)
+ dct = marcxml_to_dictionary(marcxml)
+ dublin = marcxml_dictionary_to_dc(dct)
+ # merge them
+ dct.update(dublin)
+ ranked = rank_pending_items(dct)
+ return g.render('phys/mark_arrived_choose.xhtml',
+ barcode=barcode,
+ bib_id=bib_id,
+ ranked=ranked,
+ metadata=dct)
+
+@admin_only
+def phys_mark_arrived_match(request):
+ choices = [int(k.split('_')[1]) for k in request.POST if k.startswith('choose_')]
+ if not choices:
+ return simple_message(_('No matching items selected!'),
+ _('You must select one or more matching items from the list.'))
+ else:
+ barcode = request.POST.get('barcode', '').strip()
+ assert barcode
+ smallint = request.POST.get('smallint', '').strip() or None
+ try:
+ phys = models.PhysicalObject(barcode=barcode,
+ receiver = request.user,
+ smallint = smallint)
+ phys.save()
+ except Exception, e:
+ return simple_message(_('Error'), repr(e), go_back=True)
+
+ for c in choices:
+ item = models.Item.objects.get(pk=c)
+ current_bc = item.barcode()
+ if current_bc:
+ item.metadata_set.filter(name='syrup:barcode').delete()
+ item.metadata_set.create(name='syrup:barcode', value=barcode)
+ item.save()
+ return g.render('phys/mark_arrived_outcome.xhtml')
+
+@admin_only
+def phys_circlist(request):
+ term_code = request.GET.get('term')
+ if not term_code:
+ terms = models.Term.objects.order_by('code')
+ return g.render('phys/circlist_index.xhtml', terms=terms)
+
+ term = get_object_or_404(models.Term, code=term_code)
+
+ # gather the list of wanted items for this term.
+ # Fixme, I need a better way.
+
+ cursor = django.db.connection.cursor()
+ q = "select item_id from syrup_metadata where name='syrup:barcode'"
+ cursor.execute(q)
+ bad_ids = set([r[0] for r in cursor.fetchall()])
+ cursor.close()
+
+ wanted = models.Item.objects.filter(
+ item_type='PHYS', course__term=term).select_related('metadata')
+ wanted = [w for w in wanted if w.id not in bad_ids]
+ return g.render('phys/circlist_for_term.xhtml',
+ term=term,
+ wanted=wanted)
+
+
-<?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>
-<?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> • <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> • <a href="${instance.course_url()}">Return to course page</a></p>
+ <form action="." method="POST">
+ <tr py:def="field_row(field, example=None)">
+ <th>${field.label}</th>
+ <td>
+ <ul py:if="field.errors" class="errorlist">
+ <li py:for="err in field.errors">${err}</li>
+ </ul>
+ ${Markup(field)}
+ </td>
+ <td class="example" py:if="example">e.g., ${example}</td>
+ </tr>
+ <h2>General description</h2>
+ <table class="metadata_table">
+ ${field_row(form.code, example)}
+ ${field_row(form.title)}
+ ${field_row(form.term)}
+ ${field_row(form.department)}
+ <!-- <tr><th>Department</th><td>${Markup(form.department)} ${errorlist(form.department)}</td></tr> -->
+ </table>
+ <p><input type="submit" value="Continue"/> ${go_back_link()}</p>
+ </form>
+ <div class="gap"/>
+ <div py:if="instance.id">
+ <h2>Delete this course</h2>
+ <form action="delete/" method="POST">
+ <p><input type="checkbox" name="confirm_delete" id="confirm_delete"/>
+ <label for="confirm_delete">Yes, I want to delete this course site and all of its contents.</label>
+ </p>
+ <p><input type="submit" value="Delete this course"/> ${go_back_link()}</p>
+ </form>
+ </div>
+</body>
+</html>
-<?python\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}–${min(numhits, start+limit-1)} of ${numhits} results.\r
- <span py:if="start-limit>0">\r
- <a href=".?query=${query}&start=${start-limit}&limit=${limit}">Previous ${limit}</a>\r
- •\r
- </span>\r
- <span py:if="start+limit<numhits">\r
- <a href=".?query=${query}&start=${start+limit}&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 '—'}</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}–${min(numhits, start+limit-1)} of ${numhits} results.
+ <span py:if="start-limit>0">
+ <a href=".?query=${query}&start=${start-limit}&limit=${limit}">Previous ${limit}</a>
+ •
+ </span>
+ <span py:if="start+limit<numhits">
+ <a href=".?query=${query}&start=${start+limit}&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 '—'}</td>
+ <td>
+ <form action="." method="POST">
+ <input type="hidden" name="pickitem" value="${dumps(res)}"/>
+ <input type="submit" value="Pick this item"/>
+ </form>
+ </td>
+ </tr>
+ <tr id="full_${resultnum}" style="display: none;">
+ <td colspan="4" style="padding-left: 36;">
+ <table class="metadata_table">
+ <?python allkeys = res.keys(); allkeys.sort(); ?>
+ <tr py:for="k in allkeys">
+ <th>${k}</th><td>${res[k]}</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ ${page_control()}
+ </body>
+</html>
-<?python\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
- • <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
- • <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> © 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>
+ • <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>
+ • <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> © 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>