[Jifty-commit] r4603 - in jifty/branches/virtual-models: . bin debian lib lib/Jifty/Filter lib/Jifty/Plugin lib/Jifty/Plugin/Authentication lib/Jifty/Plugin/Authentication/CAS lib/Jifty/Plugin/Authentication/CAS/Action lib/Jifty/Plugin/Authentication/CAS/Mixin lib/Jifty/Plugin/Authentication/CAS/Mixin/Model lib/Jifty/Plugin/Authentication/Ldap lib/Jifty/Plugin/Authentication/Ldap/Action lib/Jifty/Plugin/Authentication/Ldap/Mixin lib/Jifty/Plugin/Authentication/Ldap/Mixin/Model lib/Jifty/Plugin/Authentication/Ldap/doc lib/Jifty/Plugin/Authentication/Password/Action lib/Jifty/Plugin/Chart/Renderer lib/Jifty/Plugin/REST lib/Jifty/Plugin/Recorder lib/Jifty/Plugin/Recorder/Command lib/Jifty/Script lib/Jifty/Test/WWW lib/Jifty/View lib/Jifty/View/Declare lib/Jifty/View/Mason lib/Jifty/View/Static lib/Jifty/Web/Form lib/Jifty/Web/Form/Field share/po share/web/static/js share/web/static/js/scriptaculous t t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Model t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Plugin/MyAppPlugin/Model t/TestApp-Plugin-AppPluginHasModels/t t/TestApp-Plugin-OnClick/share/web/templates t/TestApp-Plugin-OnClick/t t/TestApp/lib/TestApp/Model t/TestApp/t

jifty-commit at lists.jifty.org jifty-commit at lists.jifty.org
Sat Dec 1 17:17:39 EST 2007


Author: sterling
Date: Sat Dec  1 17:17:34 2007
New Revision: 4603

Added:
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Action/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Action/CASLogin.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Action/CASLogout.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Dispatcher.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Mixin/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Mixin/Model/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Mixin/Model/User.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Action/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogin.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogout.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Dispatcher.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Mixin/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Mixin/Model/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Mixin/Model/User.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/View.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/doc/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Recorder/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Recorder.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Recorder/Command/
   jifty/branches/virtual-models/lib/Jifty/Plugin/Recorder/Command/Playback.pm
   jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Model/Texture.pm
   jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Model/Wallpaper.pm
   jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Plugin/MyAppPlugin/Model/Texture.pm
   jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/app-model-ref.t
   jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/app-model.t
   jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/OtherThingy.pm
   jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/Thingy.pm
   jifty/branches/virtual-models/t/TestApp/t/19-rightsfrom.t
Removed:
   jifty/branches/virtual-models/bin/generate-changelog
   jifty/branches/virtual-models/bin/sort-changelog
Modified:
   jifty/branches/virtual-models/   (props changed)
   jifty/branches/virtual-models/AUTHORS
   jifty/branches/virtual-models/Changelog
   jifty/branches/virtual-models/MANIFEST
   jifty/branches/virtual-models/META.yml
   jifty/branches/virtual-models/Makefile.PL
   jifty/branches/virtual-models/SIGNATURE
   jifty/branches/virtual-models/debian/control
   jifty/branches/virtual-models/lib/Jifty.pm
   jifty/branches/virtual-models/lib/Jifty/ClassLoader.pm
   jifty/branches/virtual-models/lib/Jifty/Config.pm
   jifty/branches/virtual-models/lib/Jifty/Everything.pm
   jifty/branches/virtual-models/lib/Jifty/Filter/DateTime.pm
   jifty/branches/virtual-models/lib/Jifty/Handle.pm
   jifty/branches/virtual-models/lib/Jifty/Handler.pm
   jifty/branches/virtual-models/lib/Jifty/JSON.pm
   jifty/branches/virtual-models/lib/Jifty/LetMe.pm   (props changed)
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Password/Action/Login.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Chart/Renderer/XMLSWF.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/CompressedCSSandJS.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/REST/Dispatcher.pm
   jifty/branches/virtual-models/lib/Jifty/Record.pm
   jifty/branches/virtual-models/lib/Jifty/Request.pm
   jifty/branches/virtual-models/lib/Jifty/RightsFrom.pm
   jifty/branches/virtual-models/lib/Jifty/Script/Schema.pm
   jifty/branches/virtual-models/lib/Jifty/Test.pm
   jifty/branches/virtual-models/lib/Jifty/Test/WWW/Mechanize.pm
   jifty/branches/virtual-models/lib/Jifty/Test/WWW/Selenium.pm
   jifty/branches/virtual-models/lib/Jifty/Util.pm
   jifty/branches/virtual-models/lib/Jifty/View.pm
   jifty/branches/virtual-models/lib/Jifty/View/Declare.pm
   jifty/branches/virtual-models/lib/Jifty/View/Declare/CRUD.pm
   jifty/branches/virtual-models/lib/Jifty/View/Declare/Handler.pm
   jifty/branches/virtual-models/lib/Jifty/View/Declare/Helpers.pm
   jifty/branches/virtual-models/lib/Jifty/View/Mason/Handler.pm
   jifty/branches/virtual-models/lib/Jifty/View/Static/Handler.pm
   jifty/branches/virtual-models/lib/Jifty/Web.pm
   jifty/branches/virtual-models/lib/Jifty/Web/Form/Element.pm
   jifty/branches/virtual-models/lib/Jifty/Web/Form/Field.pm
   jifty/branches/virtual-models/lib/Jifty/Web/Form/Field/Date.pm
   jifty/branches/virtual-models/lib/Jifty/Web/Form/Field/Select.pm
   jifty/branches/virtual-models/lib/Jifty/Web/Form/Link.pm
   jifty/branches/virtual-models/share/po/fr.po
   jifty/branches/virtual-models/share/web/static/js/jifty.js
   jifty/branches/virtual-models/share/web/static/js/prototype.js
   jifty/branches/virtual-models/share/web/static/js/rico.js
   jifty/branches/virtual-models/share/web/static/js/scriptaculous/effects.js
   jifty/branches/virtual-models/t/01-dependencies.t
   jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Plugin/MyAppPlugin/Model/Color.pm
   jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/plugin-model.t
   jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/share/web/templates/content.html
   jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/share/web/templates/onclick.html
   jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/t/onclick.t
   jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/User.pm
   jifty/branches/virtual-models/t/TestApp/t/11-current_user.t
   jifty/branches/virtual-models/t/TestApp/t/before_access.t

Log:
 r14323 at dynpc145:  andrew | 2007-12-01 16:01:08 -0600
 Merge down from trunk.
  r14052 at dynpc145:  andrew | 2007-11-14 20:21:23 -0600
   r14049 at dynpc145 (orig r4429):  sunnavy | 2007-11-13 13:28:29 -0600
   require CSS::Squish 0.07, no need to call _resolve_file any more
   r14050 at dynpc145 (orig r4430):  alexmv | 2007-11-13 13:31:50 -0600
    r24705 at zoq-fot-pik:  chmrr | 2007-11-13 14:31:20 -0500
     * <embed> isn't a tag which gets a close on it
   
   r14051 at dynpc145 (orig r4431):  ruz | 2007-11-14 09:17:30 -0600
   * use object calls instead of instance calls
   * use better handling of roots so server relative paths
     work as expected unless you're bind to a location
  
  r14053 at dynpc145:  andrew | 2007-11-14 20:26:49 -0600
  Adding a description to Jifty::Everything docs.
  r14054 at dynpc145:  andrew | 2007-11-14 20:31:17 -0600
  Added a synopsis and license section to docs.
  r14119 at dynpc145:  andrew | 2007-11-17 22:31:04 -0600
   r14094 at dynpc145 (orig r4435):  sunnavy | 2007-11-15 11:30:49 -0600
   merged prototype-1.6 to trunk
   r14096 at dynpc145 (orig r4437):  sunnavy | 2007-11-15 11:57:16 -0600
   not dep test for bin/sort-changelog
   r14098 at dynpc145 (orig r4439):  alexmv | 2007-11-15 14:22:53 -0600
    r24785 at zoq-fot-pik:  chmrr | 2007-11-15 15:20:31 -0500
     * Finalize triggers if possible (supported in forthcoming
       Class::Trigger release)
     * Override JDBI's default of having refers_to null return undef,
       instead of object with no id
     * RightsFrom 'foo_id' should work like RightsFrom 'foo'
   
   r14099 at dynpc145 (orig r4440):  sartak | 2007-11-15 14:42:44 -0600
    r44970 at onn:  sartak | 2007-11-09 20:47:18 -0500
    Add another changelog sorting program, this one I think works a bit better :)
   
   r14100 at dynpc145 (orig r4441):  sartak | 2007-11-15 14:42:51 -0600
    r44971 at onn:  sartak | 2007-11-09 20:47:42 -0500
    Sort change groups by header, typo fix
   
   r14101 at dynpc145 (orig r4442):  sartak | 2007-11-15 14:42:56 -0600
    r44972 at onn:  sartak | 2007-11-09 20:55:15 -0500
    Add 'performance' tag to sort-changelog
   
   r14102 at dynpc145 (orig r4443):  sartak | 2007-11-15 14:43:00 -0600
    r44981 at onn:  sartak | 2007-11-09 21:08:04 -0500
    Add a Jifty::Manual which lists each of the manuals with a short description
    Secret confession: I was tired of perldoc Jifty::Manual::<tab> failing :)
   
   r14103 at dynpc145 (orig r4444):  sartak | 2007-11-15 14:43:10 -0600
    r44996 at onn:  sartak | 2007-11-11 16:41:01 -0500
    Add changelogger dependencies
   
   r14104 at dynpc145 (orig r4445):  sartak | 2007-11-15 14:44:23 -0600
   
   r14108 at dynpc145 (orig r4449):  sartak | 2007-11-15 14:58:49 -0600
    r45207 at onn:  sartak | 2007-11-15 15:58:10 -0500
    Add TestMode: 1 to the default test config
   
   r14110 at dynpc145 (orig r4451):  sunnavy | 2007-11-16 11:35:57 -0600
   tiny typo fix
   r14111 at dynpc145 (orig r4452):  sartak | 2007-11-16 13:44:20 -0600
    r45262 at onn:  sartak | 2007-11-16 14:43:36 -0500
    Remove bin/*-changelog, as they've been moved into their own dist, App-Changelogger
   
   r14112 at dynpc145 (orig r4453):  sunnavy | 2007-11-16 13:54:06 -0600
   clean dependency test since changelogger has been moved out
   r14113 at dynpc145 (orig r4454):  sartak | 2007-11-16 14:23:18 -0600
    r45265 at onn:  sartak | 2007-11-16 14:56:01 -0500
    Remove the bin/*-changelog workaround in t/01-deps
   
   r14114 at dynpc145 (orig r4455):  sartak | 2007-11-16 14:23:33 -0600
    r45267 at onn:  sartak | 2007-11-16 15:21:43 -0500
    carp, don't warn, for a "title" Mason/TD warning
   
   r14117 at dynpc145 (orig r4458):  sartak | 2007-11-16 15:24:06 -0600
    r45274 at onn:  sartak | 2007-11-16 16:23:30 -0500
    Bump to 0.71116, update Changelog, SIGNATURE, MANIFEST, etc
   
   r14118 at dynpc145 (orig r4459):  sartak | 2007-11-16 17:39:04 -0600
    r45277 at onn:  sartak | 2007-11-16 18:38:57 -0500
    Revert r4435 because it breaks Safari 3.0.4
   
  
  r14120 at dynpc145:  andrew | 2007-11-17 22:53:30 -0600
  Adding SYNOPSIS, LICENSE, and code comments.
  r14122 at dynpc145:  andrew | 2007-11-17 22:55:17 -0600
  Stripping off the execute flag in SVN.
  r14171 at dynpc145:  andrew | 2007-11-20 13:38:04 -0600
  Assume the page is "1" if no value for page is given to the pager in CRUD.
  r14247 at dynpc145:  andrew | 2007-11-27 19:54:27 -0600
   r14126 at dynpc145 (orig r4462):  jesse | 2007-11-18 15:39:16 -0600
    r72119 at pinglin:  jesse | 2007-11-18 12:46:06 -0600
    * Added a new 'AutoUpgrade' option for Jifty and Application schemas, 
    so you don't need to manually upgrade every time jifty or your app version bumps
   
   r14127 at dynpc145 (orig r4463):  yves | 2007-11-19 05:29:56 -0600
   update fr.po and debian control
   
   r14130 at dynpc145 (orig r4466):  yves | 2007-11-19 11:17:15 -0600
   first release for an experimental mixin ldap release
   
   r14133 at dynpc145 (orig r4467):  sartak | 2007-11-19 17:46:25 -0600
    r45310 at onn:  sartak | 2007-11-19 18:46:02 -0500
    Add better support for dates in the REST plugin, rewrite a somewhat hairy method
   
   r14136 at dynpc145 (orig r4470):  ishigaki | 2007-11-20 00:48:17 -0600
   added "rel" attribute to Jifty::Web::Form::Link/Element, to allow <a rel="nofollow"...> or <a rel="next"...>
   r14180 at dynpc145 (orig r4508):  falcone | 2007-11-20 15:02:19 -0600
    r26763 at ketch:  falcone | 2007-11-20 16:00:31 -0500
    * mention Net::LDAP because of the new Plugin so we don't fail 01-dependencies
   
   r14218 at dynpc145 (orig r4536):  clkao | 2007-11-25 16:39:42 -0600
   r4534 belongs to trunk.
   
    In ClassLoader, if an action matches Create* that isn't about
    creating a model, don't just return but continue to see if there's
    any matching actions from plugins.
    
    This solves CreateOpenIDUser in particular.
   
   r14219 at dynpc145 (orig r4537):  jesse | 2007-11-25 23:48:08 -0600
    r72323 at pinglin:  jesse | 2007-11-26 00:39:46 -0500
    * A bit of refactoring to expose some more overridable templates in CRUD view
   
   r14220 at dynpc145 (orig r4538):  yves | 2007-11-26 10:41:32 -0600
   add Jasig CAS plugin with mixin model user
   
   r14226 at dynpc145 (orig r4540):  jesse | 2007-11-26 12:38:31 -0600
    r72334 at pinglin:  jesse | 2007-11-26 11:42:35 -0500
    * upgraded request logging from DEBUG to INFO
   
   r14227 at dynpc145 (orig r4541):  jesse | 2007-11-26 12:38:41 -0600
    r72335 at pinglin:  jesse | 2007-11-26 13:29:51 -0500
    * More trying to force caching of static content.
    
   
   r14229 at dynpc145 (orig r4543):  sunnavy | 2007-11-26 13:40:28 -0600
   merged prototpe-1.6 to trunk again, wish it's all right this time :)
   r14230 at dynpc145 (orig r4544):  sartak | 2007-11-26 14:14:15 -0600
    r45601 at onn:  sartak | 2007-11-26 15:13:53 -0500
    Remove some debugging statements
   
   r14231 at dynpc145 (orig r4545):  gugod | 2007-11-27 00:33:54 -0600
   Unbreak OpenID plugin with new Jifty::API implementation.
   
   Give fully-qualified action class name here because Jifty::API does
   not resolve action class names to those ones provided by plugins
   (those under Jifty::Plugin::*::Action namespace) This is maybe a
   bug, but need further discussion.
   
   r14232 at dynpc145 (orig r4546):  sunnavy | 2007-11-27 10:07:17 -0600
   make a looser check for Content-Type since firefox 3 will append charset stuff to it sometimes
   r14246 at dynpc145 (orig r4547):  sartak | 2007-11-27 19:40:29 -0600
    r45691 at onn:  sartak | 2007-11-27 20:40:06 -0500
    Add an 'as_user' method and have 'as_superuser' use it
   
  
  r14257 at dynpc145:  andrew | 2007-11-28 09:29:42 -0600
   r14256 at riddle (orig r4551):  gugod | 2007-11-28 04:45:03 -0600
   revert its previous revision for it's fixed earler in classloader.
   
  
  r14295 at dynpc145:  andrew | 2007-11-30 09:48:06 -0600
  Add support for application overridden models which gives us:
   (1) References from plugins to app models with less pain.
   (2) An easier implementation path in cases where mixins are just extra work.
   (3) Allows application flexibility to modify plugin models.
   (4) Consistency with the way apps may override actions an notifications.
  r14297 at dynpc145:  andrew | 2007-11-30 09:48:24 -0600
   r14262 at dynpc145 (orig r4553):  sartak | 2007-11-28 15:40:25 -0600
    r45710 at onn:  sartak | 2007-11-28 16:40:03 -0500
    Don't send cookies for static files. We suspect that doing so was screwing up non-compliant proxies that were caching the cookie and causing.. "amusing" security problems
   
   r14270 at dynpc145 (orig r4560):  jesse | 2007-11-28 17:39:44 -0600
    r72432 at pinglin:  jesse | 2007-11-28 18:34:07 -0500
    * Somewhat more correct handling of follow_link_ok. (Better compat with WWW::Mechanize's syntax)
    
   
   r14272 at dynpc145 (orig r4562):  jasonmay | 2007-11-28 18:45:48 -0600
   Add Jason May to AUTHORS
   
   r14273 at dynpc145 (orig r4563):  gugod | 2007-11-29 06:12:51 -0600
   A temporary fix for rico.js, which is defining the obsoleted (in
   Prototype) Object.prototype.extend which and then breaks all calls
   to jQuery.fn.extend.  That means all methods with jQuery.fn.extend
   were simply not there. For instance, jQuery("div.secret").show()
   
   r14274 at dynpc145 (orig r4564):  sartak | 2007-11-29 10:06:30 -0600
    r45710 at onn:  sartak | 2007-11-28 16:40:03 -0500
    Don't send cookies for static files. We suspect that doing so was screwing up non-compliant proxies that were caching the cookie and causing.. "amusing" security problems
   
   r14275 at dynpc145 (orig r4565):  sartak | 2007-11-29 10:07:46 -0600
    r45749 at onn:  sartak | 2007-11-29 11:06:09 -0500
    Don't set action defaults in REST. Actions can take care of themselves.
   
   r14277 at dynpc145 (orig r4567):  sartak | 2007-11-29 11:01:51 -0600
    r45754 at onn:  sartak | 2007-11-29 12:01:43 -0500
    Add a trigger to Jifty.pm to be run after all initialization (so plugins can use the database handle, or anything else)
   
   r14280 at dynpc145 (orig r4570):  sartak | 2007-11-29 12:55:19 -0600
    r45756 at onn:  sartak | 2007-11-29 13:55:10 -0500
    Rename 'ready' to 'post_init'. 'ready' and 'server_ready' are somewhat misleading because the plugins fire during other jifty commands
   
   r14281 at dynpc145 (orig r4571):  alexmv | 2007-11-29 13:20:16 -0600
    r25250 at zoq-fot-pik:  chmrr | 2007-11-29 14:19:09 -0500
     * Add in a hash of the path, so we can run the same test in parallel
       from different paths.
   
   r14290 at dynpc145 (orig r4576):  sartak | 2007-11-29 16:12:04 -0600
    r45756 at onn:  sartak | 2007-11-29 13:55:10 -0500
    Rename 'ready' to 'post_init'. 'ready' and 'server_ready' are somewhat misleading because the plugins fire during other jifty commands
   
   r14291 at dynpc145 (orig r4577):  sartak | 2007-11-29 16:12:22 -0600
    r48431 at onn:  sartak | 2007-11-29 17:11:34 -0500
    Make sure the cookie is set in the default view handler as well
   
   r14292 at dynpc145 (orig r4578):  sartak | 2007-11-29 16:12:27 -0600
    r48432 at onn:  sartak | 2007-11-29 17:11:57 -0500
    Release 0.71129
   
   r14296 at dynpc145 (orig r4580):  alech | 2007-11-30 07:14:54 -0600
   Make account confirmation error message translatable
   
  
  r14322 at dynpc145:  andrew | 2007-12-01 15:58:35 -0600
   r14299 at dynpc145 (orig r4582):  yves | 2007-11-30 11:05:09 -0600
   debian packages
   
   r14300 at dynpc145 (orig r4583):  yves | 2007-11-30 13:37:56 -0600
   allow authentication by mail or cas login and domain
   
   r14301 at dynpc145 (orig r4584):  sartak | 2007-11-30 14:23:47 -0600
    r48436 at onn:  sartak | 2007-11-30 15:23:02 -0500
    Recorder plugin: record and playback accurate HTTP requests. So far the record part is done.
   
   r14302 at dynpc145 (orig r4585):  sartak | 2007-11-30 15:19:10 -0600
    r48444 at onn:  sartak | 2007-11-30 16:18:51 -0500
    Add Jifty::Plugin::Recorder::Command::Playback with some serious caveats
   
   r14303 at dynpc145 (orig r4586):  sartak | 2007-11-30 15:54:28 -0600
    r48446 at onn:  sartak | 2007-11-30 16:54:21 -0500
    Add pid to request log name so having multiple fcgi procs won't have a cow
   
   r14304 at dynpc145 (orig r4587):  sartak | 2007-11-30 15:59:45 -0600
    r48448 at onn:  sartak | 2007-11-30 16:59:38 -0500
    Cache the log handle so the file isn't opened every hit
   
   r14305 at dynpc145 (orig r4588):  sartak | 2007-11-30 16:35:00 -0600
    r48450 at onn:  sartak | 2007-11-30 17:34:45 -0500
    Make the default log path log/requests, and make the path if needed
   
   r14306 at dynpc145 (orig r4589):  sartak | 2007-11-30 16:37:03 -0600
    r48452 at onn:  sartak | 2007-11-30 17:36:57 -0500
    Recorder: Rename handle attribute to loghandle due to a possible conflict
   
   r14307 at dynpc145 (orig r4590):  sartak | 2007-11-30 16:39:08 -0600
    r48454 at onn:  sartak | 2007-11-30 17:38:10 -0500
    Rename $pkg to $file in Jifty::Util->_require, and don't append .pm if it's already there
   
   r14308 at dynpc145 (orig r4591):  sartak | 2007-11-30 16:39:10 -0600
    r48455 at onn:  sartak | 2007-11-30 17:39:02 -0500
    Allow plugins to add new jifty commands. This requires an unreleased version of App::CLI, but otherwise works. Features usually come with a cost, and this one's cost is .02s on app startup. Worth it, IMO :)
   
   r14309 at dynpc145 (orig r4592):  sartak | 2007-11-30 16:52:24 -0600
    r48458 at onn:  sartak | 2007-11-30 17:52:18 -0500
    Bulletproof Jifty::Util->make_path
   
   r14310 at dynpc145 (orig r4593):  sartak | 2007-11-30 16:55:36 -0600
    r48460 at onn:  sartak | 2007-11-30 17:55:29 -0500
    Recorder: Don't install the trigger if creating the loghandle failed
   
   r14311 at dynpc145 (orig r4594):  sartak | 2007-11-30 17:33:02 -0600
    r48462 at onn:  sartak | 2007-11-30 18:31:36 -0500
    Allow playback of multiple files in one command, get rid of -f switch
   
   r14312 at dynpc145 (orig r4595):  sartak | 2007-11-30 17:33:16 -0600
    r48463 at onn:  sartak | 2007-11-30 18:32:55 -0500
    Recorder: Update Playback doc
   
   r14314 at dynpc145 (orig r4597):  sartak | 2007-11-30 18:10:55 -0600
    r48468 at onn:  sartak | 2007-11-30 19:10:48 -0500
    Recorder: Log all HTTP output from playback. This required a small API change to get unique filenames
   
   r14315 at dynpc145 (orig r4598):  sartak | 2007-11-30 18:28:53 -0600
    r48470 at onn:  sartak | 2007-11-30 19:28:45 -0500
    More Recorder doc
   
   r14316 at dynpc145 (orig r4599):  sartak | 2007-11-30 20:34:15 -0600
    r48483 at onn:  sartak | 2007-11-30 21:34:02 -0500
    Fix title handling in Web::Form::Field::Select
   
   r14317 at dynpc145 (orig r4600):  sartak | 2007-11-30 20:43:14 -0600
    r48485 at onn:  sartak | 2007-11-30 21:43:07 -0500
    Add a canonicalize_value to Jifty::Web::Form::Field. Override it in Date to output just the YMD part.
   
  
 


Modified: jifty/branches/virtual-models/AUTHORS
==============================================================================
--- jifty/branches/virtual-models/AUTHORS	(original)
+++ jifty/branches/virtual-models/AUTHORS	Sat Dec  1 17:17:34 2007
@@ -34,3 +34,4 @@
 Edward Funnekotter <efunneko at gmail.com>
 Cornelius Lin <c9s at aiink.com>
 Todd Chapman <todd at chaka.net>
+Jason May <jason.a.may at gmail.com>

Modified: jifty/branches/virtual-models/Changelog
==============================================================================
--- jifty/branches/virtual-models/Changelog	(original)
+++ jifty/branches/virtual-models/Changelog	Sat Dec  1 17:17:34 2007
@@ -1,3 +1,776 @@
+Jifty 0.71129
+
+I18N
+====
+
+ * Delay i18n handle init until we have session. SkeletonApp dispatcher rule
+   to set language in session. - clkao
+
+ * I18N plugin: log when user is somehow able to pass in disallowed lang.
+   inline the initial dictionary json. - clkao
+
+ * I18N: load json dict from absolute path. - clkao
+
+ * I18N::Action::SetLang: scalar::defer is trigger some oddness, rewrite
+   available languages as normal runtime code, and cached. - clkao
+
+ * I18n: fix json dictionary fallback. - clkao
+
+ * Jifty::I18N - POD fixup. - audreyt
+
+ * Jifty::I18N: Implement the L10N.AllowedLang key to limit the set of
+   languages available to the user. Use case: ja.po is incomplete and we wish
+   to hide it from users. Requested by: clkao - audreyt
+
+ * Jifty::I18N: provide available_languages method. - clkao
+
+ * Jifty::I18N::promote_encoding - r4286 by yours truly broke auto- decoding
+   for POST requests. This is a better fix to avoid the "Unquoted / not
+   allowed in Content-Type" warning (which arugably is a
+   Email::MIME::ContentType glitch), by scanning the Content- Type header for
+   the substring "charset" before parsing it. This way, when "charset" is not
+   found in that header, we still fallback to Encode::Guess, regardless of
+   whether the request was of type "multipart/form-data". - audreyt
+
+ * Jifty::I18N::promote_encoding: Multi-part form data have no notion of
+   charsets, so we return the string verbatim here, to avoid the "Unquoted /
+   not allowed in Content-Type" warnings when the Base64- encoded MIME
+   boundary string contains "/". Prompted by this Content- Type header found
+   in real world: multipart/form-data; boundary=----
+   WebKitFormBoundaryRqXyEnBQ/5VSsexe which triggered this error: --
+   2007/10/22 04:19:30 WARN> Carp.pm:46 Carp::carp Unquoted / not allowed in
+   Content-Type! at /usr/local/lib/perl5/site_perl/5.9.5/Jifty/I18N.pm line
+   226 -- 2007/10/22 04:19:30 WARN> Carp.pm:46 Carp::carp Illegal Content-
+   Type parameter /5VSsexe at
+   /usr/local/lib/perl5/site_perl/5.9.5/Jifty/I18N.pm line 226 - audreyt
+
+ * Jifty::Plugin::I18N: provides SetLang action. provides loc.js and other
+   facilities to l10n your javascript along with po. - clkao
+
+ * Provide get_current language method in Jifty::I18N. - clkao
+
+ * Support serving json version of po and localization in javascript
+   space. - clkao
+
+ * Update zh-tw po to include translations for authentication::password
+   plugin. - clkao
+
+ * allow switching current language for Localization. - clkao
+
+ * bin/jifty po now takes two more options: --dir for additional directories
+   to look at so javascript files can be scanned. --js for generating json
+   dictionaries for messages appeared in javascript files declared with
+   Jifty::Web. - clkao
+
+ * loc.js cleanups. - clkao
+
+ * updated ja.po - ishigaki
+
+ * zh_tw l10n for Authenication::Password plugin. - clkao
+
+BUGFIX
+======
+
+ * A temporary fix for rico.js, which is defining the obsoleted (in
+   Prototype) Object.prototype.extend which and then breaks all calls
+   to jQuery.fn.extend.  That means all methods with jQuery.fn.extend
+   were simply not there. For instance, jQuery("div.secret").show() - gugod
+
+ * Added missing jifty-result-popup div to the page detritus section of
+   Jifty::View::Declare::Page. - sterling
+
+ * Allow users to name their apps in lowercase (as pragmas ?!). Reported by
+   SteveH++ - jesse
+
+ * Bring the default back, but at 0.0.0 to prevent uninitialized complaints.
+   - sterling
+
+ * Class::Accessor::Fast cleanups - jesse
+
+ * Clean up Jifty::Web initialization a bit. - jesse
+
+ * Clean up the masonXXXXXXXXXX temp directory after each test run. - sterling
+
+ * Do not do clever & broken input_name for buttons when preserve_state is
+   false. - clkao
+
+ * Don't do weird things with loading model subclasses - jesse
+
+ * Don't send cookies for static files. We suspect that doing so was screwing up
+   non-compliant proxies that were caching the cookie and causing.. "amusing"
+   security problems - sartak
+
+ * Fix compatibility with Test::WWW::Mechanize - jesse
+
+ * Fix mismerge.. - sartak
+
+ * Fixes to several of my fixes for
+   Jifty::Request::load_from_data_structure - jesse
+
+ * Fixing a bug that prevents any but the first plugin being checked for
+   schema updates. - sterling
+
+ * Fixing a typo in the plugin DB version metadata key affecting upgrades.
+   - sterling
+
+ * Horrible Hash::Merge workaround, part 2 - alexmv
+
+ * Improving schema setup for plugins that are turned on after the application
+   is initially deployed. - sterling
+
+ * Jifty::Request::Mapper - Avoid uninitialized warnings when
+   $args{destination} ends up undefined. - audreyt
+
+ * Make sure Jifty::Filter::DateTime doesn't disturb Floating datetimes
+   Otherwise, dates would get set to the current_user's timezone, throwing a
+   wrench in the works - sartak
+
+ * Menu class is undef by default; fall back to the empty string - alexmv
+
+ * Minor edit: fixing reference to Jifty::Request::Action to Jifty::Action
+   - sterling
+
+ * Pg wants to connect to template1, but CREATE DATABASE foo TEMPLATE
+   template0 - alexmv
+
+ * Plugin upgrade incorrectly assumes a DB version of 0.0.1 when none is
+   found. - sterling
+
+ * Rather than helpfully failing to load model classes that have
+   compiletime errors, actually die with the error that caused the class
+   not to load. - jesse
+
+ * Remove debug info. - clkao
+
+ * Remove debugging statement - alexmv
+
+ * Removing the display style from the jifty-result-popup, which is breaking
+   it. - sterling
+
+ * Revert clkao's fixes for now because they break apps :/ - sartak
+
+ * Revert r4117 as the element readiness state is not compatible with what it
+   was. - clkao
+
+ * Revert r4435 because it breaks Safari 3.0.4 - sartak
+
+ * Reverting the addition of these files which were added by a push error.
+   - sterling
+
+ * Some databases have schema creation single-threadedness. If they block,
+   wait and retry for up to a minute - jesse
+
+ * Some parts of that last commit to use only one Module::Pluggable weren't as
+   hot as they appeared to be; - jesse
+
+ * The JIFTY_SITE_CONFIG environment variable was silently ignored. - audreyt
+
+ * Undoing 4302. There is such a class. Thanks to chmrr. - sterling
+
+ * Undoing previous accidental patch to Jifty::Continuation because I have not
+   finished proofing it. - sterling
+
+ * Use ->clone on DateTime objects to avoid possible DST issues, etc - sartak
+
+ * Use blessed() instead of ref() to keep an if statement from tripping on
+   ->isa(). - sterling
+
+ * We depend on the bugfix that
+   File::Spec->rel2abs("/foo","/foo") eq "." - alexmv
+
+ * When it's a ::Create action, $event_info won't have a record_id there,
+   cause a fatal error when it's published latter. - gugod
+
+ * When loading a web request from a data structure, don't totally throw away
+   the path it was requested at. - jesse
+
+ * Work around a "feature" in Hash::Merge < 0.10 - alexmv
+
+ * carp, don't warn, for a "title" Mason/TD warning - sartak
+
+ * collection generator may be called when we're loading models and not all
+   models have been loaded to the moment, so we have to try to require the
+   model before testing if it esists. - ruz
+
+ * don't gen a collection class if there is no model - ruz
+
+ * don't warn if database doesn't exist when dropping database - sunnavy
+
+ * friendly_date should be relative to the user, not floating - sartak
+
+ * make a looser check for Content-Type since firefox 3 will append charset
+   stuff to it sometimes - sunnavy
+
+ * merged prototpe-1.6 to trunk again, wish it's all right this time :) -
+   sunnavy
+
+ * no need to print STDERR, we can warn in $SIG{__WARN__} - sunnavy
+
+ * oops, should look for js files under js. - clkao
+
+ * our version of prototype can't deal with synchronous request. - clkao
+
+ * revert changes to ClassLoader. Collection classes are failing to be
+   generated in some TD views - falcone
+
+ * rollback r4242 which cached the gzipped js/css even through develmode
+   reload. - clkao
+
+ * tabview shouldn't force absolute paths to relative ones - jesse
+
+ * warnings should be to STDERR - sunnavy
+
+ * we want to compute UUIDs every time. really. it's not useful to always have
+   the same uuid - jesse
+
+ * webservices_rediret: don't redirect if there's any failed action. - clkao
+
+CORE
+====
+
+ * Add Jifty::DateTime::from_epoch which does our usual timezone magic, use
+   set_current_user_timezone more, too - sartak
+
+ * Add Jifty::Util->is_app_root to determine whether a path looks enough like
+   the app root - sartak
+
+ * Add a 'now' method to Jifty::DateTime which sets the timezone to the
+   current user's timezone. DateTime::now passes time_zone => UTC to
+   Jifty::DateTime::now, which caused it to skip over the currentuser/timezone
+   magic. - sartak
+
+ * Add a ForwardCompatible config in Database section. The usecase is that
+   when you switch your checkout to something with older version of app
+   schema, you can declare it is compatiable with the future version that is
+   currently in db. - clkao
+
+ * Add a Jifty::DateTime set_current_user_timezone method - sartak
+
+ * Add an 'as_user' method and have 'as_superuser' use it - sartak
+
+ * Add the ability for apps to have very fine-grained testing (such as
+   overriding config for a subdir of t/, or even a particular test file).
+   Still needs tests and I suppose some more doc :) - sartak
+
+ * Added a new 'AutoUpgrade' option for Jifty and Application schemas,   so you
+   don't need to manually upgrade every time jifty or your app version bumps -
+   jesse
+
+ * Added the ability to craft a Jifty button with a "submit" link which
+   submits hardcoded arguments. - jesse
+
+ * Change how plugin is initialized so _pre_init is actually noted. - clkao
+
+ * Don't require recipients in Jifty::Notificaiton be objects. - clkao
+
+ * Finalize triggers if possible (supported in forthcoming Class::Trigger
+   release). Override JDBI's default of having refers_to null return undef,
+   instead of object with no id. RightsFrom 'foo_id' should work like
+   RightsFrom 'foo' - alexmv
+
+ * Fix the problem of tests having both config and a correctly-relative $0
+   Jifty::SubTest now makes available the old Cwd (which as 03-nosubtest
+   demonstrates doesn't break apps that don't use Jifty::SubTest) Also a bit
+   of doc in Jifty::Config The Feature is Done. :) - sartak
+
+ * Fixes to not cache compressed css+js while in devel mode. Refactoring js
+   compression code into the plugin where it belongs. - jesse
+
+ * Get rid of another place where we had a custom accessor that meant
+   'arguments' - for great consistency - jesse
+
+ * In Jifty::DateTime->new, set the timezone to floating if it looks like just
+   a date - sartak
+
+ * Initial implementation of Jifty::Test::WWW::Declare, with basic tests in
+   TestApp - sartak
+
+ * Just a perltidy - alexmv
+
+ * Make action_arguments effective from js update() - clkao
+
+ * Making the as_*_action() methods accept a paramhash for additional
+   new_action() parameters. - sterling
+
+ * New as_string method for Jifty::Web::Form::Link. - clkao
+
+ * Override T:W:D's get with ours which prepends the server's URL Start
+   documentation - sartak
+
+ * Recursively transform content structures. Set a flag when we change the
+   method, in case apps care - alexmv
+
+ * Remove the unused drop-column logic from jifty schema --setup, since
+   ->columns returns only active cols - sartak
+
+ * Ripping out new_record_action() from Jifty::Web. Re-implementing it as
+   as_create_action(), as_update_action(), as_delete_action, and
+   as_search_action() in Jifty::Record and Jifty::Collection. - sterling
+
+ * Test config loading is now complete code-wise :) And it has tests now too!
+   Just need to doc it in manual or Jifty::Config - sartak
+
+ * Untabify - alexmv
+
+ * Updated the JSON and YAML transports for web service requests to allow
+   complex data structures in actions - jesse
+
+ * Updating the Jifty web menu component to respect classes added by
+   users - jesse
+
+ * added "rel" attribute to Jifty::Web::Form::Link/Element, to allow
+   <a rel="nofollow"...> or <a rel="next"...> - ishikaga
+
+ * allow onsubmit in form to include custom javascript. - clkao
+
+ * if refers_to is not mandatory field then add 'no_value' option to
+   select box - ruz
+
+ * merged prototype-1.6 to trunk - sunnavy
+
+ * use object calls instead of instance calls. use better handling of roots
+   so server relative paths work as expected unless you're bind to a
+   location - ruz
+
+ * working toward onclick => { submit => { action => $action, arguments
+   => { foo => 'value', bar => 'other value'} }} Jifty's js still needs
+   help - jesse
+
+CRUD
+====
+
+ * A bit of refactoring to expose some more overridable templates in CRUD view
+   - jesse
+
+ * Adding another CRUD setup example to the synopsis. - sterling
+
+ * Assume the page is "1" if no value for page is given to the pager in CRUD.
+   - sterling
+
+ * Don't override object_type if the CRUD view package already defines
+   object_type. - sterling
+
+ * Simplifying the CRUD code for generating the record_class and making it
+   more flexible. Adding a stub for CRUD testing. - sterling
+
+ * added a bit more semantic markup to the crud view - jesse
+
+DOC
+===
+
+ * A lot of POD for OAuth, and some tiny fixes elsewhere - sartak
+
+ * Add a Jifty::Manual which lists each of the manuals with a short
+   description. Secret confession: I was tired of perldoc Jifty::Manual::<tab>
+   failing :) - sartak
+
+ * Add Jason May to AUTHORS - sartak
+
+ * Added a synopsis and license section to docs. - sterling
+
+ * Adding SYNOPSIS, LICENSE, and code comments. - sterling
+
+ * Adding a description to Jifty::Everything docs. - sterling
+
+ * Adding documentation for Action and Static attributes to resolve POD
+   coverage test failures. - sterling
+
+ * Another example app: ShrinkURL, which is basically (surprise!)
+   tinyurl - sartak
+
+ * Clean up the Pod, added a Synopsis, added a Why? section, added code
+   comments, and some code tidying. - sterling
+
+ * Cleaning up documentation to fix pod coverage test failures. - sterling
+
+ * Doc - sartak
+
+ * Fix Jifty::Server::Prefork's document so it mentions Net::Server::PreFork,
+   instead of Net::Server::Prefork. (Yes this is very confusing.) - audreyt
+
+ * Fix package names, add copyright notices - sartak
+
+ * Fixing POD coverage errors in the AutoReference plugin. - sterling
+
+ * Fixing pod coverage problems with the jQuery plugin. - sterling
+
+ * Improving POD and minor tidying. - sterling
+
+ * Include the http method (GET, POST, etc) in the debug message - sartak
+
+ * Jifty::Notification - Minor doc fix - The html_body method was misspelled
+   as html-body. - audreyt
+
+ * More comments - sartak
+
+ * POD clean-up, some code commenting, and minor tidying. - sterling
+
+ * POD for LeakTracker's subs - sartak
+
+ * POD re-wrapping. perltidy. Use Jifty::DBI's new ->_new_record_args and
+   _new_collection_args. Bump the JDBI dep accordingly - alexmv
+
+ * SendFeedback.pm: Take away the phrase "hiveminder" from the
+   documentation. - audreyt
+
+ * a tiny change and typo fix - sunnavy
+
+ * doc fixes - sunnavy
+
+ * pod fixes - jesse
+
+ * tiny typo fix - sunnavy
+
+ * typo fix - sunnavy
+
+ * updated my email address in AUTHORS to agentzh at agentzh.org - agentz
+
+INSTALL
+=======
+
+ * mention Net::LDAP because of the new Plugin so we don't fail
+   01-dependencies - falcone
+
+ * Add changelogger dependencies - sartak
+
+ * Added requirement for URI 1.31, which adds uri_escape_utf8(). - sterling
+
+ * Adding File::Temp 0.15 requirement for cleanup(). - sterling
+
+ * Bump JDBI dep to 0.45 (Filter::DateTime changes) - sartak
+
+ * Force a clone which doesn't have as many bugs - alexmv
+
+ * Jifty::DBI now uses Class::Trigger instead of Jifty::DBi::Class::Trigger
+   (since rev3968) - ishigaki
+
+ * Make Test::WWW::Declare an optional dep for now - sartak
+
+ * Modules other than Net::OAuth::Request in Net-OAuth distro don't have
+   version - ishigaki
+
+ * Move memleak deps into its own Makefile.PL section - sartak
+
+ * Removing Test::Log4perl. It isn't used anywhere anymore. - sterling
+
+ * Require Net::OAuth 0.04 because 0.03 needs my patch. Don't run tests if
+   Net::OAuth is uninstalled - sartak
+
+ * added Test::Log4perl dep again; it is surely used at t/TestApp/t/02-dispatch-show-rule-in-wrong-
+   ruleset.t - ishigaki
+
+ * clean dependency test since changelogger has been moved out - sunnavy
+
+ * older Email::LocalDelivery is buggy. - clkao
+
+ * require CSS::Squish 0.07, no need to call _resolve_file any more - sunnavy
+
+ * we need DateTime::Locale - sunnavy
+
+MISC
+====
+
+ * A command line completion script is added into contrib/ - c9s
+
+ * Add 'performance' tag to sort-changelog - sartak
+
+ * Add another changelog sorting program, this one I think works a bit better
+   :) - sartak
+
+ * Remove bin/*-changelog, as they've been moved into their own dist, App-
+   Changelogger - sartak
+ 
+ * Remove some debugging statements - sartak
+
+ * Sort change groups by header, typo fix - sartak
+
+ * Stripping off the execute flag in SVN. - sterling
+
+ * add a bug tag - falcone
+
+ * add an exclude command for logs you don't need to ship - falcone
+
+ * fix so we don't try to print to a bogus filehandle - falcone
+
+ * make format_entry return text rather than printing. use the returned
+   message to generate the changelog rather than printing to STDOUT - falcone
+
+ * remove doubled loop over log messages. explicitly loop over
+   uncategorized first, then offer the user to review all the changes
+   again. reformatting - falcone
+
+ * talk about how to do iterative editing - falcone
+
+ * update fr.po and debian control - yves
+
+ * upgraded request logging from DEBUG to INFO - jesse
+
+ * usage documentation - falcone
+
+ * use existing t-discard tag. don't autoskip things, just group them together
+   and you can delete them - falcone
+
+PERFORMANCE
+===========
+
+ * The cache was actually a huge performance problem - jesse
+
+ * Use much less UNIVERSAL::require (to shave a second or two off start
+   times) - jesse
+
+ * When replacing a region, use the technique described in
+   http://blog.stevenlevithan.com/archives/faster-than-innerhtml to improve
+   performance. - clkao
+
+ * Cache gzipped output from the compressedcssandjs plugin - jesse
+
+ * Fix a bug that after you access a static css file, it breaks the compressed
+   css by not actually squishing main.css, hence requests static css files
+   under __jifty/css. - clkao
+
+ * In Jifty::Model::Session: Turn session_id, data_key, key_type into
+   case_sensitive, so we don't do useless tolower on loading sessions. index
+   session_id by default. - clkao
+
+ * Switch to a single call to Module::Pluggable to help improve startup
+   performance. - jesse
+
+ * More trying to force caching of static content. - jesse
+
+PLUGIN
+======
+
+ * Add a trigger to Jifty.pm to be run after all initialization (so plugins can
+   use the database handle, or anything else) - sartak
+
+ * Add an additional trigger in Clickable for SinglePage to filter out state
+   variables for region-__page. - clkao
+
+ * Add better support for dates in the REST plugin, rewrite a somewhat hairy
+   method - sartak
+
+ * Add cdn option to CompressedCSSandJS plugin. - clkao
+
+ * Add new LeakDetector plugin. It kinda sorta works :) - sartak
+
+ * Added a new plugin that provides an autocomplete widget specially for
+   record references. Helpful in cases when a select is just be too big to
+   contemplate. - sterling
+
+ * Adding missing JavaScript file and cleaning up a method left-over from
+   testing. - sterling
+
+ * Don't set action defaults in REST. Actions can take care of themselves. -
+   sartak
+
+ * Expanded the previous fix to cover sticky values and the initial value in
+   the input tag. - sterling
+
+ * Fix it so that AutoReference fields show the _brief_description rather than
+   the id in read mode. - sterling
+
+ * Fixed AutoReference so that it is possible to set the field to empty if not
+   mandatory. - sterling
+
+ * Fixed a problem where AutoReference breaks in Search actions. - sterling
+
+ * Fixed the feedback plugin so it works even if the current user doesn't have
+   permission to read their own email. - chapman
+
+ * Fixing the data fetcher on the AutoReference box so that it sends the text
+   being typed rather than the current value in the hidden field. - sterling
+
+ * Get rid of spurious warning in Auth::Password plugin - sartak
+
+ * Handle region redirect with continuation return (for spa). - clkao
+
+ * Implement callback URLs and test them - sartak
+
+ * Include the list of object types leaked in the request report - sartak
+
+ * Make find_plugins() sensitive to wantarray. - sterling
+
+ * Many improvements, including some pages - sartak
+
+ * Modified the JS to ignore the [id:1] text at the end of the references.
+   - sterling
+
+ * More cleanup, hide zero-leak requests by default - sartak
+
+ * More fixes to get protected resource requests going. We might need a
+   WWW::Mechanize::OAuth subclass, so that each get/post will be properly
+   wrapped as an OAuth request - sartak
+
+ * Move files from LeakDetector to LeakTracker (because that's the term Cat
+   uses) - sartak
+
+ * Name the first Jifty->new during test 'pre_init', and tell plugins not to
+   register triggers just yet to avoid duplicated triggers. - clkao
+
+ * Now we send responses to token requests (and test them) What's left:
+   Authorizing request tokens is coded, but it's slightly flawed somehow
+   Getting access tokens is coded, but not tested No code yet for the actual
+   accessing of resources - sartak
+
+ * Patch by Alex to make sp_submit_form to be happier with J:V vars. - clkao
+
+ * Provide an after_include_javascript for plugins to do javascript
+   initialisation. - clkao
+
+ * Rename 'ready' to 'post_init'. 'ready' and 'server_ready' are somewhat
+   misleading because the plugins fire during other jifty commands - sartak
+
+ * Refactor timestamp and nonce out of tokens and into consumers. Thanks to
+   hannesty for discussing how it should work - sartak
+
+ * Some cleanups in LeakDetector - sartak
+
+ * Some more fleshing out of the OAuth plugin - sartak
+
+ * Somehow this file got duplicated. I might've done it. shrug - sartak
+
+ * Start adding an OAuth plugin - sartak
+
+ * Start writing tests Change timestamp to (ugh) time_stamp, because timestamp
+   is a reserved word in a few SQL apps - sartak
+
+ * The REST dispatcher should warn $@ if an error occurs - sartak
+
+ * Turns out we can't (so easily) have the URLs be configurable. Not too much
+   of a problem though Other fixes - sartak
+
+ * Unbreak OpenID plugin with new Jifty::API implementation.  Give
+   fully-qualified action class name here because Jifty::API does not resolve
+   action class names to those ones provided by plugins (those under
+   Jifty::Plugin::*::Action namespace) This is maybe a bug, but need further
+   discussion. - gugod
+
+ * Users can now authorize/deny request tokens - sartak
+
+ * Various improvements of try_oauth so it gets further (but not all the way)
+   in the correct codepath - sartak
+
+ * Various refactors and cleanups Request tokens are now properly set to
+   "used" Life of a request token lengthened on authorize Fixed duplicate
+   timestamp/nonce check (and test) - sartak
+
+ * Whoops, committed in a subdirectory - sartak
+
+ * Whoops, the consumer and user were using the same mech. But fixing that
+   didn't break anything :) - sartak
+
+ * actormetadata: provide a current_user_is_owner method. - clkao
+
+ * add Jasig CAS plugin with mixin model user - yves
+
+ * default not to preserve state when spa is replacing __page - clkao
+
+ * first release for an experimental mixin ldap release - yves
+
+ * r4534 belongs to trunk. In ClassLoader, if an action matches Create* that
+   isn't about creating a model, don't just return but continue to see if
+   there's any matching actions from plugins.  This solves CreateOpenIDUser in
+   particular.  - jesse
+
+ * revert its previous revision for it's fixed earler in classloader. - gugod
+
+ * simplify ccjs plugin with static handler method call. - clkao
+
+ * spa: don't translate javascript: links. - clkao
+
+ * tabview: fix the logic for defered tabs. - clkao
+
+TESTING
+=======
+
+ * Add (grr, passing) tests for setting columns in the test app - sartak
+
+ * Add TestMode: 1 to the default test config - sartak
+
+ * Add some more tests for Jifty::DateTime in the TestApp - sartak
+
+ * Begin adding tests for protected resource requests - sartak
+
+ * Clearing out old new_record_action() tests. - sterling
+
+ * Correctly skip OAuth tests in the absence of Net::OAuth - sartak
+
+ * Dispatcher and config fixes, start adding a new test file - sartak
+
+ * Expand the tests, now they actually test something real! So right now the
+   OAuth plugin is rejecting a few kinds of bad requests (e.g. unknown
+   consumer) while accepting a good request. Now to make more requests of each
+   type :) - sartak
+
+ * First cut of Selenium testing support in Jifty. - clkao
+
+ * Fix test count - alexmv
+
+ * Fixing long-broken support for the "JIFTY_FASTTEST" env variable - jesse
+
+ * Mailboxes need to have unique names for parallel testing - alexmv
+
+ * More tests, start implementing callbacks, but failing :) - sartak
+
+ * Most AccessToken tests done :) first implementation was right on - sartak
+
+ * Refactor the tests - sartak
+
+ * Skip OAuth tests if Crypt::OpenSSL::RSA is unavailable RSA should be
+   optional, will fix later - sartak
+
+ * The first few test scripts are now complete, and uncovered many bugs.
+   Yay. - sartak
+
+ * When testing, don't just use a single global test database. Instead, use
+   one per testfile. This allows some measure of parallelization - jesse
+
+ * Wrote most of the authorization tests - sartak
+
+ * added TestApp-Plugin-OnClick, initially for the update of
+   prototype.js - sunnavy
+
+ * clean dependency test since changelogger has been moved out - sunnavy
+
+ * first cut of tests for singlepage app plugin. - clkao
+
+ * my computer is slow. be more patient about selenium rc startup. - clkao
+
+ * not dep test for bin/sort-changelog - sunnavy
+
+ * only run pod coverage tests if you're a developer - jesse
+
+ * refactor Jifty::Test a bit - sunnavy
+
+ * support SELENIUM_RC_TEST_AGAINST and SELENIUM_RC_BROWSER environment
+   variable, and allow args to be passed into selenium rc invoker. - clkao
+
+VIEW
+====
+
+ * <embed> isn't a tag which gets a close on it - alexmv
+
+ * Add a hook to change a menu item's label - sartak
+
+ * Add support for "title" attribute on elements, which shows tooltips -
+   sartak
+
+ * Added an app_page_footer page call to jifty view declare page - jesse
+
+ * Additional selector to make sure that autocomplete hides .hidden_value on
+   .inline forms. - sterling
+
+ * In TD View handler, don't just return without printing headers if the
+   response content is empty, as when you access /__jifty/empty. - clkao
+
+ * Making Jifty::Web::Menu more extensible. - sterling
+
+ * Upgrade a region error from a debug to a warning. - jesse
+
+
+
+
+
 Jifty 0.70824
 
 INSTALL

Modified: jifty/branches/virtual-models/MANIFEST
==============================================================================
--- jifty/branches/virtual-models/MANIFEST	(original)
+++ jifty/branches/virtual-models/MANIFEST	Sat Dec  1 17:17:34 2007
@@ -1,11 +1,11 @@
 AUTHORS
 bin/build_par
-bin/generate-changelog
 bin/jifty
 bin/runcover
 bin/service
 bin/xgettext
 Changelog
+contrib/jifty_completion.sh
 debian/changelog
 debian/compat
 debian/control
@@ -93,6 +93,13 @@
 examples/Ping/share/web/templates/index.html
 examples/Ping/t/00compile.t
 examples/Ping/t/01startup.t
+examples/ShrinkURL/bin/jifty
+examples/ShrinkURL/etc/config.yml
+examples/ShrinkURL/lib/ShrinkURL/Action/CreateShrunkenURL.pm
+examples/ShrinkURL/lib/ShrinkURL/Dispatcher.pm
+examples/ShrinkURL/lib/ShrinkURL/Model/ShrunkenURL.pm
+examples/ShrinkURL/lib/ShrinkURL/View.pm
+examples/ShrinkURL/Makefile.PL
 examples/Yada/bin/jifty
 examples/Yada/etc/config.yml
 examples/Yada/inc/Module/Install.pm
@@ -156,6 +163,7 @@
 lib/Jifty/JSON.pm
 lib/Jifty/LetMe.pm
 lib/Jifty/Logger.pm
+lib/Jifty/Manual.pm
 lib/Jifty/Manual/AccessControl.pod
 lib/Jifty/Manual/Actions.pod
 lib/Jifty/Manual/Continuations.pod
@@ -187,12 +195,23 @@
 lib/Jifty/Plugin/ActorMetadata.pm
 lib/Jifty/Plugin/ActorMetadata/Mixin/Model/ActorMetadata.pm
 lib/Jifty/Plugin/AdminUI.pm
+lib/Jifty/Plugin/Authentication/CAS.pm
+lib/Jifty/Plugin/Authentication/CAS/Action/CASLogin.pm
+lib/Jifty/Plugin/Authentication/CAS/Action/CASLogout.pm
+lib/Jifty/Plugin/Authentication/CAS/Dispatcher.pm
+lib/Jifty/Plugin/Authentication/CAS/Mixin/Model/User.pm
 lib/Jifty/Plugin/Authentication/Facebook.pm
 lib/Jifty/Plugin/Authentication/Facebook/Action/LinkFacebookUser.pm
 lib/Jifty/Plugin/Authentication/Facebook/Action/LoginFacebookUser.pm
 lib/Jifty/Plugin/Authentication/Facebook/Dispatcher.pm
 lib/Jifty/Plugin/Authentication/Facebook/Mixin/Model/User.pm
 lib/Jifty/Plugin/Authentication/Facebook/View.pm
+lib/Jifty/Plugin/Authentication/Ldap.pm
+lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogin.pm
+lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogout.pm
+lib/Jifty/Plugin/Authentication/Ldap/Dispatcher.pm
+lib/Jifty/Plugin/Authentication/Ldap/Mixin/Model/User.pm
+lib/Jifty/Plugin/Authentication/Ldap/View.pm
 lib/Jifty/Plugin/Authentication/Password.pm
 lib/Jifty/Plugin/Authentication/Password/Action/ConfirmEmail.pm
 lib/Jifty/Plugin/Authentication/Password/Action/GeneratePasswordToken.pm
@@ -208,6 +227,8 @@
 lib/Jifty/Plugin/Authentication/Password/Notification/ConfirmEmail.pm
 lib/Jifty/Plugin/Authentication/Password/Notification/ConfirmLostPassword.pm
 lib/Jifty/Plugin/Authentication/Password/View.pm
+lib/Jifty/Plugin/AutoReference.pm
+lib/Jifty/Plugin/AutoReference/Widget.pm
 lib/Jifty/Plugin/Chart.pm
 lib/Jifty/Plugin/Chart/Dispatcher.pm
 lib/Jifty/Plugin/Chart/Renderer.pm
@@ -231,9 +252,22 @@
 lib/Jifty/Plugin/GoogleMap.pm
 lib/Jifty/Plugin/GoogleMap/Widget.pm
 lib/Jifty/Plugin/Halo.pm
+lib/Jifty/Plugin/I18N.pm
+lib/Jifty/Plugin/I18N/Action/SetLang.pm
 lib/Jifty/Plugin/JQuery.pm
+lib/Jifty/Plugin/LeakTracker.pm
+lib/Jifty/Plugin/LeakTracker/Dispatcher.pm
+lib/Jifty/Plugin/LeakTracker/View.pm
 lib/Jifty/Plugin/LetMe.pm
 lib/Jifty/Plugin/LetMe/Dispatcher.pm
+lib/Jifty/Plugin/OAuth.pm
+lib/Jifty/Plugin/OAuth/Action/AuthorizeRequestToken.pm
+lib/Jifty/Plugin/OAuth/Dispatcher.pm
+lib/Jifty/Plugin/OAuth/Model/AccessToken.pm
+lib/Jifty/Plugin/OAuth/Model/Consumer.pm
+lib/Jifty/Plugin/OAuth/Model/RequestToken.pm
+lib/Jifty/Plugin/OAuth/Token.pm
+lib/Jifty/Plugin/OAuth/View.pm
 lib/Jifty/Plugin/OnlineDocs.pm
 lib/Jifty/Plugin/OnlineDocs/Dispatcher.pm
 lib/Jifty/Plugin/OpenID.pm
@@ -293,7 +327,9 @@
 lib/Jifty/Subs.pm
 lib/Jifty/Subs/Render.pm
 lib/Jifty/Test.pm
+lib/Jifty/Test/WWW/Declare.pm
 lib/Jifty/Test/WWW/Mechanize.pm
+lib/Jifty/Test/WWW/Selenium.pm
 lib/Jifty/TestServer.pm
 lib/Jifty/Upgrade.pm
 lib/Jifty/Upgrade/Internal.pm
@@ -945,6 +981,7 @@
 share/plugins/Jifty/Plugin/AdminUI/web/templates/__jifty/admin/fragments/list/view
 share/plugins/Jifty/Plugin/AdminUI/web/templates/__jifty/admin/index.html
 share/plugins/Jifty/Plugin/AdminUI/web/templates/__jifty/admin/model/dhandler
+share/plugins/Jifty/Plugin/AutoReference/web/static/js/autoreference.js
 share/plugins/Jifty/Plugin/Chart/web/static/css/simple_bars.css
 share/plugins/Jifty/Plugin/Chart/web/static/flash/xmlswf/charts.swf
 share/plugins/Jifty/Plugin/Chart/web/static/flash/xmlswf/charts_library/arno.swf
@@ -974,6 +1011,7 @@
 share/plugins/Jifty/Plugin/Chart/web/static/js/simple_bars.js
 share/plugins/Jifty/Plugin/GoogleMap/web/static/css/google_map.css
 share/plugins/Jifty/Plugin/GoogleMap/web/static/js/google_map.js
+share/plugins/Jifty/Plugin/I18N/web/static/js/loc.js
 share/plugins/Jifty/Plugin/JQuery/web/static/js/jquery.js
 share/plugins/Jifty/Plugin/JQuery/web/static/js/noConflict.js
 share/plugins/Jifty/Plugin/OnlineDocs/web/templates/__jifty/online_docs/autohandler
@@ -1176,6 +1214,10 @@
 t/TestApp-Plugin-Chart/Makefile.PL
 t/TestApp-Plugin-Chart/t/chart.t
 t/TestApp-Plugin-Chart/t/gd_graph.t
+t/TestApp-Plugin-CompressedCSSandJS/bin/jifty
+t/TestApp-Plugin-CompressedCSSandJS/etc/config.yml
+t/TestApp-Plugin-CompressedCSSandJS/Makefile.PL
+t/TestApp-Plugin-CompressedCSSandJS/t/css.t
 t/TestApp-Plugin-JQuery/bin/jifty
 t/TestApp-Plugin-JQuery/etc/config.yml
 t/TestApp-Plugin-JQuery/Makefile.PL
@@ -1185,6 +1227,28 @@
 t/TestApp-Plugin-News/lib/TestApp/Plugin/News/Model/News.pm
 t/TestApp-Plugin-News/lib/TestApp/Plugin/News/View.pm
 t/TestApp-Plugin-News/Makefile.PL
+t/TestApp-Plugin-OAuth/bin/jifty
+t/TestApp-Plugin-OAuth/etc/config.yml
+t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Dispatcher.pm
+t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Model/User.pm
+t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Test.pm
+t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/View.pm
+t/TestApp-Plugin-OAuth/Makefile.PL
+t/TestApp-Plugin-OAuth/t/00-test-setup.t
+t/TestApp-Plugin-OAuth/t/01-basic.t
+t/TestApp-Plugin-OAuth/t/02-request-token.t
+t/TestApp-Plugin-OAuth/t/03-authorize.t
+t/TestApp-Plugin-OAuth/t/04-access-token.t
+t/TestApp-Plugin-OAuth/t/05-protected-resource.t
+t/TestApp-Plugin-OAuth/t/id_rsa
+t/TestApp-Plugin-OAuth/t/id_rsa.pub
+t/TestApp-Plugin-OnClick/bin/jifty
+t/TestApp-Plugin-OnClick/etc/config.yml
+t/TestApp-Plugin-OnClick/Makefile.PL
+t/TestApp-Plugin-OnClick/share/web/templates/content.html
+t/TestApp-Plugin-OnClick/share/web/templates/content1.html
+t/TestApp-Plugin-OnClick/share/web/templates/onclick.html
+t/TestApp-Plugin-OnClick/t/onclick.t
 t/TestApp-Plugin-PasswordAuth/bin/jifty
 t/TestApp-Plugin-PasswordAuth/etc/config.yml
 t/TestApp-Plugin-PasswordAuth/lib/TestApp/Plugin/FasterSwallow.pm
@@ -1209,11 +1273,21 @@
 t/TestApp-Plugin-REST/t/00-prototype.t
 t/TestApp-Plugin-REST/t/01-config.t
 t/TestApp-Plugin-REST/t/02-basic-use.t
+t/TestApp-Plugin-SinglePage/bin/jifty
+t/TestApp-Plugin-SinglePage/etc/config.yml
+t/TestApp-Plugin-SinglePage/lib/TestApp/Plugin/SinglePage/Model/User.pm
+t/TestApp-Plugin-SinglePage/lib/TestApp/Plugin/SinglePage/View.pm
+t/TestApp-Plugin-SinglePage/Makefile.PL
 t/TestApp/bin/jifty
+t/TestApp/etc/config.yml
+t/TestApp/etc/site_config.yml
 t/TestApp/lib/TestApp/Action/DoSomething.pm
 t/TestApp/lib/TestApp/Action/DoSomethingElse.pm
+t/TestApp/lib/TestApp/Action/SayHi.pm
 t/TestApp/lib/TestApp/CurrentUser.pm
 t/TestApp/lib/TestApp/Dispatcher.pm
+t/TestApp/lib/TestApp/Model/OtherThingy.pm
+t/TestApp/lib/TestApp/Model/Thingy.pm
 t/TestApp/lib/TestApp/Model/User.pm
 t/TestApp/lib/TestApp/Upgrade.pm
 t/TestApp/lib/TestApp/View.pm
@@ -1261,11 +1335,20 @@
 t/TestApp/t/15-template-subclass.t
 t/TestApp/t/16-template-region.t
 t/TestApp/t/17-template-region-internal-redirect.t
+t/TestApp/t/18-test-www-declare.t
+t/TestApp/t/19-rightsfrom.t
 t/TestApp/t/before_access.t
 t/TestApp/t/config-Cachable
 t/TestApp/t/config-Record
+t/TestApp/t/config/01-basic.t
+t/TestApp/t/config/02-individual.t
+t/TestApp/t/config/02-individual.t-config.yml
+t/TestApp/t/config/03-nosubtest.t
+t/TestApp/t/config/test_config.yml
+t/TestApp/t/crud.t
 t/TestApp/t/i18n-standalone.t
 t/TestApp/t/instance_id.t
 t/TestApp/t/regex_meta_in_path_info.t
+t/TestApp/t/test_config.yml
 t/TestApp/t/upgrade.t
 t/TestApp/t/use_mason_wrapper.t

Modified: jifty/branches/virtual-models/META.yml
==============================================================================
--- jifty/branches/virtual-models/META.yml	(original)
+++ jifty/branches/virtual-models/META.yml	Sat Dec  1 17:17:34 2007
@@ -21,6 +21,7 @@
     - DB
 recommends: 
   Apache2::Const: 0
+  Authen::CAS::Client: 0
   Cache::FileCache: 0
   Chart::Base: 0
   Class::Accessor::Named: 0
@@ -40,6 +41,7 @@
   Module::CoreList: 0
   Module::Install::Admin: 0.50
   Module::Refresh: 0.09
+  Net::LDAP: 0
   Net::OAuth::AccessTokenRequest: 0
   Net::OAuth::ProtectedResourceRequest: 0
   Net::OAuth::Request: 0.04
@@ -53,13 +55,14 @@
   Test::HTTP::Server::Simple: 0.02
   Test::MockModule: 0.05
   Test::MockObject: 1.07
+  Test::WWW::Declare: 0.01
   WWW::Facebook::API: 0.3.6
   XML::Simple: 0
 requires: 
   App::CLI: 0.03
   CGI: 3.19
   CGI::Cookie::Splitter: 0
-  CSS::Squish: 0.05
+  CSS::Squish: 0.07
   Cache::Cache: 0
   Calendar::Simple: 0
   Class::Accessor: 0
@@ -75,8 +78,9 @@
   Data::UUID: 0
   Date::Manip: 0
   DateTime: 0
+  DateTime::Locale: 0
   Email::Folder: 0
-  Email::LocalDelivery: 0
+  Email::LocalDelivery: 0.217
   Email::MIME: 0
   Email::MIME::ContentType: 0
   Email::MIME::CreateHTML: 0
@@ -90,6 +94,7 @@
   File::MMagic: 0
   File::ShareDir: 0.04
   File::Spec: 3.14
+  File::Temp: 0.15
   HTML::Entities: 0
   HTML::Lint: 0
   HTML::Mason: 1.3101
@@ -103,7 +108,7 @@
   IPC::PubSub: 0.23
   IPC::Run3: 0
   JSON::Syck: 0.15
-  Jifty::DBI: 0.44
+  Jifty::DBI: 0.47
   LWP::UserAgent: 0
   Locale::Maketext::Extract: 0.20
   Locale::Maketext::Lexicon: 0.60
@@ -128,7 +133,6 @@
   Test::LongString: 0
   Test::More: 0.62
   Test::Pod::Coverage: 0
-  Test::WWW::Declare: 0
   Test::WWW::Mechanize: 1.04
   Test::WWW::Selenium: 0
   UNIVERSAL::require: 0
@@ -142,4 +146,4 @@
   perl: 5.8.3
   version: 0
 tests: t/*.t t/*/*.t t/*/*/*.t t/*/*/*/*.t
-version: 0.70824
+version: 0.71129

Modified: jifty/branches/virtual-models/Makefile.PL
==============================================================================
--- jifty/branches/virtual-models/Makefile.PL	(original)
+++ jifty/branches/virtual-models/Makefile.PL	Sat Dec  1 17:17:34 2007
@@ -15,7 +15,7 @@
 requires('Crypt::CBC');
 requires('Crypt::Rijndael');
 requires('Compress::Zlib');
-requires('CSS::Squish' => 0.05 );
+requires('CSS::Squish' => 0.07 );
 requires('DBD::SQLite' => 1.11 );
 requires('Data::Page');
 requires('Data::UUID');
@@ -181,6 +181,14 @@
         recommends('Crypt::OpenSSL::RSA'),
         recommends('Digest::HMAC_SHA1'),
     ],
+    'Ldap Plugin' => [
+        -default => 0,
+        recommends('Net::LDAP')
+    ],
+    'CAS Plugin' => [
+        -default => 0,
+        recommends('Authen::CAS::Client')
+    ]
 );
 
 

Modified: jifty/branches/virtual-models/SIGNATURE
==============================================================================
--- jifty/branches/virtual-models/SIGNATURE	(original)
+++ jifty/branches/virtual-models/SIGNATURE	Sat Dec  1 17:17:34 2007
@@ -14,23 +14,23 @@
 -----BEGIN PGP SIGNED MESSAGE-----
 Hash: SHA1
 
-SHA1 17176d35b4f890942d4925d177aa5ee738bc8087 AUTHORS
-SHA1 cb3f86d393183a717f1a46f824c6aaf0d9aade49 Changelog
-SHA1 badc3476b33310cfcb8260c1fc108196acf5f54f MANIFEST
+SHA1 811e053b197e04b6ea152e1b212adb1c7507ccf9 AUTHORS
+SHA1 093a640d507f1ff3f6b438860683fef8a721382c Changelog
+SHA1 8845354b5f04bcecb8d07bdab38bc446cb6889ad MANIFEST
 SHA1 d4adbf5948041cd460da5cb7ad21394a790e2022 MANIFEST.SKIP
-SHA1 64dceae5347277836a681070a3da49806f374db8 META.yml
-SHA1 7e06c5f49d98ac8808694f6b41246fdb0de5e564 Makefile.PL
+SHA1 5ec992734aee97a590332e18ff9f74a16697be0d META.yml
+SHA1 a13dd59bdff88ec645f5866ecb03f1f06e58bfd0 Makefile.PL
 SHA1 e395a2eabaf8faf8266dedc664c1eb52c6c589cf README
 SHA1 aaf8f7a1025fc97077072672f325e2a5f3c03a41 bin/build_par
-SHA1 36c2486a0102933488558d0734b9e222d53a059b bin/generate-changelog
 SHA1 c1ff9ff7f2a88bc4306b3866b6b80fb9aa8e8423 bin/jifty
 SHA1 bc5d0dc181bffe0694e5282c2d2336eaec5a06ff bin/runcover
 SHA1 9a91a81e3db1a12368153fed9e504aad492cd971 bin/service
 SHA1 543a2677f66d3c8ca671b790509b6c1721ac6270 bin/xgettext
+SHA1 807b1f94b379fd537b780ecf43b0e6642ec00f4d contrib/jifty_completion.sh
 SHA1 1c042485ba8a21f0f124dd8ed412d43d3805430e debian/README
 SHA1 abd0bc47e8f6e42610dc1349564f9ab2a71d3857 debian/changelog
 SHA1 5d9474c0309b7ca09a182d888f73b37a8fe1362c debian/compat
-SHA1 d12729a01ca782356f865b814b6ed0395220dfa0 debian/control
+SHA1 e0add5d722eb64c040e08f6089c32c1b4aaf31f5 debian/control
 SHA1 8fc130ffa6d53c47d94eab1616887c511d54d61f debian/rules
 SHA1 b8bb315ef8fbdd2f069b6339ad0461b5d933d7da doc/ajax-upgraded-links
 SHA1 a0d03921821ca39ea461f1f9fa812fdbb5f501a0 doc/building_a_par
@@ -114,6 +114,13 @@
 SHA1 05ac283a14e76750d63678d0864a0403aa18fd28 examples/Ping/share/web/templates/index.html
 SHA1 53c8822ddf426e82fe239e4470574913411b1355 examples/Ping/t/00compile.t
 SHA1 6d6c378447f9d74d53c06a2a027736b48b5d0670 examples/Ping/t/01startup.t
+SHA1 c291f424a0f57833ca391656383d3659ee30518f examples/ShrinkURL/Makefile.PL
+SHA1 c1ff9ff7f2a88bc4306b3866b6b80fb9aa8e8423 examples/ShrinkURL/bin/jifty
+SHA1 3167862eb1985f70a569077d4bdaabf2414e0be8 examples/ShrinkURL/etc/config.yml
+SHA1 f8e845578ba384150af148e1b5704ab67b79d365 examples/ShrinkURL/lib/ShrinkURL/Action/CreateShrunkenURL.pm
+SHA1 6c9d7448f280583d437ce45bfca5f6fa9e2a965f examples/ShrinkURL/lib/ShrinkURL/Dispatcher.pm
+SHA1 17c44063396555e73d65214d587b8231cdbed140 examples/ShrinkURL/lib/ShrinkURL/Model/ShrunkenURL.pm
+SHA1 d8b943e2fd860ac4215aec02cb86c95c4f859212 examples/ShrinkURL/lib/ShrinkURL/View.pm
 SHA1 0d7b61a9ed50444ec98fd289965a8ce9b6d672bd examples/Yada/META.yml
 SHA1 f3ef104950d63f97e7af2ed9bf7dc3f1cf2b80e2 examples/Yada/Makefile.PL
 SHA1 f7f44f9a7337def0c97f981073e3ed970851d9ae examples/Yada/bin/jifty
@@ -148,35 +155,36 @@
 SHA1 02af973fae2ac3531fa6b704574b2b8cb2a08148 inc/Module/Install/Win32.pm
 SHA1 3a2eab96e91cca8d99938cda7791759ae9d97b3a inc/Module/Install/WriteAll.pm
 SHA1 c17e8f3cf8ebe1eb4929fd2bd2fd530a9de1abd0 lib/Email/Send/Jifty/Test.pm
-SHA1 08ba4fd288d0a91c59a9c1995bf654123f7b7a7e lib/Jifty.pm
+SHA1 8672e31dca656481a815ad83c90d6e1b973515f5 lib/Jifty.pm
 SHA1 2d1948051cad65db5062c8f884ec42c85bd82ff1 lib/Jifty/API.pm
 SHA1 0cc91932ee9cf2a93da8004bf4e170f5de391652 lib/Jifty/Action.pm
 SHA1 14be4def02c11173c504eff60256df952b3c305f lib/Jifty/Action/Autocomplete.pm
-SHA1 6e368525e04d4733923682f9c314a0cde41c4f48 lib/Jifty/Action/Record.pm
+SHA1 c9e1cab1d36b20eb587baad7f99b98f03d564902 lib/Jifty/Action/Record.pm
 SHA1 47d9ab2a7703960be292d141e69fc65c528deb1e lib/Jifty/Action/Record/Create.pm
 SHA1 b5556afc5639dc3c5bd4ea8979ab6665ca6503d4 lib/Jifty/Action/Record/Delete.pm
 SHA1 4f2fabd16d6e799b51e7161e488e71c9c608e32a lib/Jifty/Action/Record/Search.pm
-SHA1 165b1e15b83e1c34f8ec5a192b50e67b348ab7f5 lib/Jifty/Action/Record/Update.pm
+SHA1 ac1190edc6cdbc2eea3077448c60c53b1b5ce524 lib/Jifty/Action/Record/Update.pm
 SHA1 90399bcddb1172f0c61a3fc357538ba08a3a8254 lib/Jifty/Action/Redirect.pm
 SHA1 57f95b0fbfd55d09d079d7c200d3a2451764c40a lib/Jifty/Bootstrap.pm
-SHA1 92ed1f1ada38336b639ec0a7f519151ab3ec1fff lib/Jifty/ClassLoader.pm
-SHA1 9765c0412c127e3b7a35c49d5ad5ce4313ccd3b7 lib/Jifty/Client.pm
-SHA1 7ead6f337c0665069bfa9e58915e6f22ed83a16c lib/Jifty/Collection.pm
-SHA1 4d3c3b93ef230ce507de998fac9996037b4237b0 lib/Jifty/Config.pm
-SHA1 576f5ed38bf71febf19674e021c7ee919e38eff2 lib/Jifty/Continuation.pm
-SHA1 db9b2fcce2eba256349d6bdb5fb4a3c9b395ab0a lib/Jifty/CurrentUser.pm
-SHA1 2afb9e7c7bafe56dd12986ad99653880c5873a70 lib/Jifty/DateTime.pm
-SHA1 cef0407ef9bb8cf4c4e0ee349fdcb32046e7c216 lib/Jifty/Dispatcher.pm
+SHA1 85f498567ff72c3f5e8b0f873fc5ac6609c020be lib/Jifty/ClassLoader.pm
+SHA1 c8f230dab4bb43b096a2e5ef90fb9f5f5810ebde lib/Jifty/Client.pm
+SHA1 f2dbb796ad56ace11459e428a823141049633878 lib/Jifty/Collection.pm
+SHA1 b53571ad4675919d91476a08dfc58f7f3c50ffe7 lib/Jifty/Config.pm
+SHA1 ece59049dead78c5de734ea9027299de419d1149 lib/Jifty/Continuation.pm
+SHA1 52767831d767d8b48aee80df53dc9a1eb4ced0e6 lib/Jifty/CurrentUser.pm
+SHA1 1ff73e9d244e21fcd1e0f851feede37f069d8281 lib/Jifty/DateTime.pm
+SHA1 efae9f981fcfda810181aee8fa8bf7d3bed372ff lib/Jifty/Dispatcher.pm
 SHA1 e50bd211339e464cd5e17e05a12157e4f9bb5494 lib/Jifty/Event.pm
 SHA1 a956a886b552b6abf1e8c1485297c1bb86ff438c lib/Jifty/Event/Model.pm
-SHA1 121cc604741f5a674cbbc2a55dfb6d4c8cf11bb8 lib/Jifty/Everything.pm
-SHA1 818bd0aa6afeb39bf96e0068fe3222c74133b4d8 lib/Jifty/Filter/DateTime.pm
-SHA1 53c0eb3380e3f1611866694a6e1294f3537f0d76 lib/Jifty/Handle.pm
-SHA1 886936781f71637eaf4e2f644032ba16194571e9 lib/Jifty/Handler.pm
-SHA1 24be261cc518d7d36bed0caebe79943b0e549b39 lib/Jifty/I18N.pm
-SHA1 fe370e2c51ca0f20cb0bce133b8c9067c02a524a lib/Jifty/JSON.pm
+SHA1 8052721bf1354b96fdc98f23b0bffa32595d514f lib/Jifty/Everything.pm
+SHA1 6357a183fc7b6d7f49b314c40198550ba1b6f972 lib/Jifty/Filter/DateTime.pm
+SHA1 92c091b9820bb36dadd39a5a8aa69210390573bf lib/Jifty/Handle.pm
+SHA1 a8722a1b1cbdcd1f86f0cdf37c5f75b5730e5bfa lib/Jifty/Handler.pm
+SHA1 551ca6606a300a0048feeee1e176422d2f0f9b5f lib/Jifty/I18N.pm
+SHA1 0bd413f3995d98f1da897b65e483680e4bf79a0f lib/Jifty/JSON.pm
 SHA1 7083562103a0feb46db9bd0bd2323fe62dcb1b0c lib/Jifty/LetMe.pm
 SHA1 1687a979438f24c95a6b857ab5305910108bfb49 lib/Jifty/Logger.pm
+SHA1 cc180ff223629e6c1c7b5f2becac4bde27f0fa69 lib/Jifty/Manual.pm
 SHA1 9a01a2e586aa5799fdc0f04c6240220d9dd953b9 lib/Jifty/Manual/AccessControl.pod
 SHA1 ef9ff36385a9f780ac0204bffb9425818d78b789 lib/Jifty/Manual/Actions.pod
 SHA1 44c07eb236063f6978a945d0faabdcd2b4799126 lib/Jifty/Manual/Continuations.pod
@@ -197,23 +205,34 @@
 SHA1 d0b7a4277c8a3d1a393d7c51fff48b059461f87c lib/Jifty/Manual/UsingCSSandJS.pod
 SHA1 e8e0a5e342506f3656f7b642a0915b9339da3e2c lib/Jifty/Mason/Halo.pm
 SHA1 c7a9988b0826f9c55af2415c7d314ff546193cee lib/Jifty/Model/Metadata.pm
-SHA1 4b89786af61d1490286bc02c4c3a8edb95aa0b60 lib/Jifty/Model/Session.pm
+SHA1 9612083e329a3366aed947c725340b5b858eca66 lib/Jifty/Model/Session.pm
 SHA1 869954cc1c1a636bf45aa43fa3e565ba19e39007 lib/Jifty/Model/SessionCollection.pm
-SHA1 4d70ace015a3d020687aab814dae62f86d051428 lib/Jifty/Module/Pluggable.pm
-SHA1 88f2aacbfaeca55afa86f6b21bab250ce37f35ea lib/Jifty/Notification.pm
+SHA1 bb261929a2f58a0dbda883febc7842ae6db88aa4 lib/Jifty/Module/Pluggable.pm
+SHA1 b70ca16929be0c714bbb60403b253ceae5fb613e lib/Jifty/Notification.pm
 SHA1 445f707ba7577b529bf7ca9ddcd2587250d0b272 lib/Jifty/Object.pm
 SHA1 c3fde2a862013cd6284637d79c751c3c2e360720 lib/Jifty/Param.pm
-SHA1 c4c0387640f7eafb35b346798c5e150c6ecb7496 lib/Jifty/Param/Schema.pm
-SHA1 9ec6c409d8b0bc974d4feee07363ea458c30453f lib/Jifty/Plugin.pm
+SHA1 07d789d9e304da8e632ce1236dda4a3e7fab8fc7 lib/Jifty/Param/Schema.pm
+SHA1 ea8bc6f6595d6dcab26a5457638bd233d6b476d4 lib/Jifty/Plugin.pm
 SHA1 e5af7b18d906a1e69fc96d97f13c3d003fe9f217 lib/Jifty/Plugin/ActorMetadata.pm
-SHA1 ed6aa344eca5c227c0b2e94f40c7f73455c93bea lib/Jifty/Plugin/ActorMetadata/Mixin/Model/ActorMetadata.pm
+SHA1 3932c22836be32e8d9ffcb2776dfb03ed4c8a2de lib/Jifty/Plugin/ActorMetadata/Mixin/Model/ActorMetadata.pm
 SHA1 328b724a1f266999eb1f12e60a7ac6fcc533d177 lib/Jifty/Plugin/AdminUI.pm
+SHA1 e8e9401a9ec0ac20a0996f534ead47b2d5acddac lib/Jifty/Plugin/Authentication/CAS.pm
+SHA1 2c2ac2ad4e7c38db8a27251f18a0f2ca54250bb9 lib/Jifty/Plugin/Authentication/CAS/Action/CASLogin.pm
+SHA1 efa71a233b50313e954f5e2aa4866e1b97bffa3b lib/Jifty/Plugin/Authentication/CAS/Action/CASLogout.pm
+SHA1 96cfd68c312a6431277bfca496150a2e0f4acade lib/Jifty/Plugin/Authentication/CAS/Dispatcher.pm
+SHA1 c248a17f3e6659b5729c4d010d3dcf459602bb6c lib/Jifty/Plugin/Authentication/CAS/Mixin/Model/User.pm
 SHA1 a4cf065e1e54ac09a715d89b107768b0ae19e0b6 lib/Jifty/Plugin/Authentication/Facebook.pm
 SHA1 eb0ac23054f98ef6ae30ae06a03adaf896fd601c lib/Jifty/Plugin/Authentication/Facebook/Action/LinkFacebookUser.pm
 SHA1 ac51405e49435ce2745458f7e1fa9f0534eceb53 lib/Jifty/Plugin/Authentication/Facebook/Action/LoginFacebookUser.pm
 SHA1 ec31d1cac498f4cb7763807eb5cf34aa8a9c01e5 lib/Jifty/Plugin/Authentication/Facebook/Dispatcher.pm
 SHA1 672cb1f77ca40e0a6fb2d8088cfb38ba9e31e0e8 lib/Jifty/Plugin/Authentication/Facebook/Mixin/Model/User.pm
 SHA1 1bfe9c2762606521f55072a011b9878234f3c5f8 lib/Jifty/Plugin/Authentication/Facebook/View.pm
+SHA1 7f705218d54a5344316d65e14bd652d4813d8f7c lib/Jifty/Plugin/Authentication/Ldap.pm
+SHA1 4d361ea2dbc725e85867b4ed513fda24ba29b182 lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogin.pm
+SHA1 ce2ef5a7ef9bc29a6f6610ce7e9b41cdc52ce857 lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogout.pm
+SHA1 739e9d55d4be4f94fa3cf58d9c5d7b03baba1217 lib/Jifty/Plugin/Authentication/Ldap/Dispatcher.pm
+SHA1 f4b4308dd2b41acccabc973244b012463cf05160 lib/Jifty/Plugin/Authentication/Ldap/Mixin/Model/User.pm
+SHA1 016f41981e6c400f728d4f03b24c290f4a57f114 lib/Jifty/Plugin/Authentication/Ldap/View.pm
 SHA1 e435ff3b0c7b8a79d5b1d25802f4e2918bc94087 lib/Jifty/Plugin/Authentication/Password.pm
 SHA1 e9c5a3f4263412e003ff55a88b794424608008dc lib/Jifty/Plugin/Authentication/Password/Action/ConfirmEmail.pm
 SHA1 de9208f8911bc09594299b618a7ce05c71cac57e lib/Jifty/Plugin/Authentication/Password/Action/GeneratePasswordToken.pm
@@ -222,13 +241,15 @@
 SHA1 074fe2fa59464a78b69558f2c20b39fe45a6e56e lib/Jifty/Plugin/Authentication/Password/Action/ResendConfirmation.pm
 SHA1 eb20b698db12db185f08e4d0e0a6c39dc2c1432f lib/Jifty/Plugin/Authentication/Password/Action/ResetLostPassword.pm
 SHA1 204602d1b7b07f7c98b1369a25c18099e3ea5100 lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm
-SHA1 28604b89817992c647042420090a59fee0fe06bd lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm
+SHA1 2a539002f3fd0b07c9132610f4cb01625aea3291 lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm
 SHA1 3d0b444280c3b0ae577e0ada5cb3fd2eb8803f9f lib/Jifty/Plugin/Authentication/Password/Action/Signup.pm
 SHA1 e18879da7a75bd2fe96dcc380a72d005a3a82aad lib/Jifty/Plugin/Authentication/Password/Dispatcher.pm
-SHA1 ea71250fa9cf6185d97a0b05529b20f92297f122 lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm
+SHA1 60cd4b172ffd30856756a944075fb6d0c528d3ed lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm
 SHA1 9711ece6f3637e972708c8bda313aaa57fd1b405 lib/Jifty/Plugin/Authentication/Password/Notification/ConfirmEmail.pm
 SHA1 f8e1724e5d0d3e490439f0ad761a96155ff1b5a4 lib/Jifty/Plugin/Authentication/Password/Notification/ConfirmLostPassword.pm
 SHA1 ed84b2845c3b6b53e20f420862f8f8a23a569700 lib/Jifty/Plugin/Authentication/Password/View.pm
+SHA1 33e798ef8c54a46e9332c5c38a7dcfaf8b3aee26 lib/Jifty/Plugin/AutoReference.pm
+SHA1 bc86ea08d5055c8488f1836c8a3fa2c87975db30 lib/Jifty/Plugin/AutoReference/Widget.pm
 SHA1 632df071506dc232b7855f31e216a05055262874 lib/Jifty/Plugin/Chart.pm
 SHA1 8d018ee02a46e1ab758d2096b9a1039550dfe913 lib/Jifty/Plugin/Chart/Dispatcher.pm
 SHA1 4bc7a990867f0482b2c49f8cd28e328fb5947797 lib/Jifty/Plugin/Chart/Renderer.pm
@@ -236,25 +257,38 @@
 SHA1 ec469e4ef188a1fdeadd208974ffa2c0afa79f2c lib/Jifty/Plugin/Chart/Renderer/GD/Graph.pm
 SHA1 5f5c9a658bef0074b24ec8567305085b28022cf2 lib/Jifty/Plugin/Chart/Renderer/PlotKit.pm
 SHA1 3e11c9832ed05ad5cbb4095081654d53284628e5 lib/Jifty/Plugin/Chart/Renderer/SimpleBars.pm
-SHA1 332109e5a4e6b464814a6331fcbe1ddeec5d8502 lib/Jifty/Plugin/Chart/Renderer/XMLSWF.pm
+SHA1 387e83601ac32063ed23927a90bce4c7c5b8fc5c lib/Jifty/Plugin/Chart/Renderer/XMLSWF.pm
 SHA1 436d7ef252dfe555b0d0d56cf126d30cad59728f lib/Jifty/Plugin/Chart/View.pm
 SHA1 e842dafa977259712540df4639249689e3bd1c90 lib/Jifty/Plugin/Chart/Web.pm
 SHA1 da70c7fbcd78d7ae9bd53deb74a022d0eeea4a12 lib/Jifty/Plugin/ClassLoader.pm
-SHA1 471913e19c92eebdcc3a338b8fdfdccbbb647959 lib/Jifty/Plugin/CompressedCSSandJS.pm
-SHA1 3547e9d0a2e53ae2c57cb07ba6ef88fa21ef8c28 lib/Jifty/Plugin/CompressedCSSandJS/Dispatcher.pm
+SHA1 f0390d573033b27d73e2ba1ab1dddaeabdf786df lib/Jifty/Plugin/CompressedCSSandJS.pm
+SHA1 3143510bdf480a1d42ea4199119d808443c3246b lib/Jifty/Plugin/CompressedCSSandJS/Dispatcher.pm
 SHA1 f7c65a1e0fd9a9cc3a69238e5d0c94cfdacbf484 lib/Jifty/Plugin/Debug.pm
 SHA1 bccc0b4e51396e7b3d95d43ce75774d922a4fc14 lib/Jifty/Plugin/Debug/Dispatcher.pm
 SHA1 b6a34618e94885dac38d0a6b501a63f5c0184008 lib/Jifty/Plugin/ErrorTemplates.pm
 SHA1 23acc252dd633a0c6873acadced84729dd6be728 lib/Jifty/Plugin/ErrorTemplates/View.pm
-SHA1 9afb7bc33b82dbc0f6de63c75b674c71858275eb lib/Jifty/Plugin/Feedback.pm
-SHA1 453a08b54bfd5cba652d304f4731f9208364a17e lib/Jifty/Plugin/Feedback/Action/SendFeedback.pm
+SHA1 2ce2a6c92f94c786e60d995ff88ac4c61538ef7a lib/Jifty/Plugin/Feedback.pm
+SHA1 c11790aee7ac9c20ea1dea6dbcdc392e25fb393b lib/Jifty/Plugin/Feedback/Action/SendFeedback.pm
 SHA1 1c214a74f063384a7e59e516f917131a2338ace3 lib/Jifty/Plugin/Feedback/View.pm
 SHA1 d4f28de89e6bc1bde247a7b5e4e78b19961772e0 lib/Jifty/Plugin/GoogleMap.pm
 SHA1 e79fe2283de73ff1d369e386129ff0adfdfad61b lib/Jifty/Plugin/GoogleMap/Widget.pm
 SHA1 f13f769cb4d6d529bcfc46647512544eaaa54774 lib/Jifty/Plugin/Halo.pm
+SHA1 2ba0c0d41adf3f9ae6102c4ee0339c69cb1fa41c lib/Jifty/Plugin/I18N.pm
+SHA1 9ab115aaab5a08b1b25e4acfdb338c736df13af1 lib/Jifty/Plugin/I18N/Action/SetLang.pm
 SHA1 668a78f235a1b025f9d15328f646df5578178f3b lib/Jifty/Plugin/JQuery.pm
+SHA1 2b148ed613cee998ac790f8dca27148fd2521b2d lib/Jifty/Plugin/LeakTracker.pm
+SHA1 c6cc61f0b8e1d83dfb6ab54653c65322df50cabc lib/Jifty/Plugin/LeakTracker/Dispatcher.pm
+SHA1 fc29cecc82021c3079b9375ca86b911c56e731ab lib/Jifty/Plugin/LeakTracker/View.pm
 SHA1 432d5c6343b4390dcd135918d7f729d2c01eee5c lib/Jifty/Plugin/LetMe.pm
 SHA1 8692eb8735e17f6f9a25f41c368da4a6839363c8 lib/Jifty/Plugin/LetMe/Dispatcher.pm
+SHA1 0179fb90782af6cf4f8f0336d9ce4123f2fa4ee6 lib/Jifty/Plugin/OAuth.pm
+SHA1 ec118e0358980bd9095d4258c025177b340d241e lib/Jifty/Plugin/OAuth/Action/AuthorizeRequestToken.pm
+SHA1 97383f671fd34c2ed8c26cc98f0d7e557e51268e lib/Jifty/Plugin/OAuth/Dispatcher.pm
+SHA1 906219fce92eca4d483e4b571280a60b5320ecd3 lib/Jifty/Plugin/OAuth/Model/AccessToken.pm
+SHA1 a0a2965b3511771a3ac32e659adb33bc572b6ebd lib/Jifty/Plugin/OAuth/Model/Consumer.pm
+SHA1 6d90773d54a5cea5fc7ea35c1240bd5b5987f1ba lib/Jifty/Plugin/OAuth/Model/RequestToken.pm
+SHA1 0ecb471c00653630443029761dce4816f8b308d3 lib/Jifty/Plugin/OAuth/Token.pm
+SHA1 ef4c52b621a5b0af3dcd5668036ac46d479a31c8 lib/Jifty/Plugin/OAuth/View.pm
 SHA1 af733b25eee3cc4e0e6b087a76bfd965534a2343 lib/Jifty/Plugin/OnlineDocs.pm
 SHA1 fcc3ccbee7c9646745fc79475d53f67e81d7c0db lib/Jifty/Plugin/OnlineDocs/Dispatcher.pm
 SHA1 c39eb405a1d25e84b582868e12279fcbdcede688 lib/Jifty/Plugin/OpenID.pm
@@ -265,18 +299,18 @@
 SHA1 fe37000b633c08c77eaf6adb6cf0847a26450eb8 lib/Jifty/Plugin/OpenID/Mixin/Model/User.pm
 SHA1 1a30f243c1f3efa0fd045f922eb5f062c80d80b9 lib/Jifty/Plugin/OpenID/View.pm
 SHA1 df332565b291fd458fab232414b24283f379ac16 lib/Jifty/Plugin/REST.pm
-SHA1 056a963c9277e8f0cfba399b2c9b66531a2987c5 lib/Jifty/Plugin/REST/Dispatcher.pm
-SHA1 d2dd17c683748f9bbeb831ab6dc9f3a873fa1514 lib/Jifty/Plugin/SinglePage.pm
+SHA1 7f4df605ecaa0dd18e50e4d12b13ef6a87307976 lib/Jifty/Plugin/REST/Dispatcher.pm
+SHA1 decbf7728e192b9fcb3f8d330279315bae429c3f lib/Jifty/Plugin/SinglePage.pm
 SHA1 23720e49b68018925083f408d10de34d0f297042 lib/Jifty/Plugin/SinglePage/Dispatcher.pm
 SHA1 f866590b203c18013cc2f667de7b889e84895670 lib/Jifty/Plugin/SiteNews.pm
 SHA1 b72aeb9e89b787e0feb81e421c5e63de36f48726 lib/Jifty/Plugin/SiteNews/Dispatcher.pm
 SHA1 654eec3ba80aa6199c558ac13484308a214a1219 lib/Jifty/Plugin/SiteNews/Mixin/Model/News.pm
 SHA1 a3bf9e62b68d625ac0da1fd3b571d91258b303d1 lib/Jifty/Plugin/SiteNews/View/News.pm
 SHA1 7fa0c5fa1ef6dbe8f1c4a351133f74ca32a70507 lib/Jifty/Plugin/SkeletonApp.pm
-SHA1 274d797b34ef684c58df7db353346973f8e1bbc2 lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm
-SHA1 f9e1002e94b28aaac932ecf09421e984ba02d1e3 lib/Jifty/Plugin/SkeletonApp/View.pm
-SHA1 5fdd02b758f8cbeb06a9012d4e28c76064e3d6b9 lib/Jifty/Plugin/TabView/View.pm
-SHA1 cbfe62b83a5ea089e1536ca54babf24006652406 lib/Jifty/Plugin/UUID.pm
+SHA1 63165112948dd17a86a0544ff60f82d4ed89a777 lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm
+SHA1 af327531d97512491cf14c42bad1dc2c606eab89 lib/Jifty/Plugin/SkeletonApp/View.pm
+SHA1 812425a5f9a9a108a3d878e347e77dcb532ae19a lib/Jifty/Plugin/TabView/View.pm
+SHA1 df4d15dcb23b75e7b785cfbc282a3bc1e6c37a18 lib/Jifty/Plugin/UUID.pm
 SHA1 e58f034ec726d72becad8ff361097c4f115549f8 lib/Jifty/Plugin/UUID/Widget.pm
 SHA1 5ce0a4dd684e7a9029f67a3679f268acbd1cbf31 lib/Jifty/Plugin/User.pm
 SHA1 5811d61115c8162417885dec09180ab3f356a962 lib/Jifty/Plugin/User/Mixin/Model/User.pm
@@ -285,17 +319,17 @@
 SHA1 37016b10d1f2f0cc479b549009d1307237099f22 lib/Jifty/Plugin/Userpic/View.pm
 SHA1 5712e2f84235c7b57e38ac85513bbecbdc15dfdd lib/Jifty/Plugin/Userpic/Widget.pm
 SHA1 c77500585cddf1c520b0438384481a1b68d13c57 lib/Jifty/Plugin/Yullio/View.pm
-SHA1 ae4a12074e7e1da9bf9628ecd4dbf36cac8369f6 lib/Jifty/Record.pm
-SHA1 a63b8179f42e38ebcd84ed803a15a75b9c4f84f5 lib/Jifty/Request.pm
-SHA1 574126a02a3c625710a849c1f9f7fd0dd226759b lib/Jifty/Request/Mapper.pm
+SHA1 fd9d89d56afac0140b7f9b37d6b0a10ec9d6c34c lib/Jifty/Record.pm
+SHA1 976757105f990db0f882cb7227e92f0709f59271 lib/Jifty/Request.pm
+SHA1 86d7ce58578cd3a06e17e7d3c8314e0741ba68f8 lib/Jifty/Request/Mapper.pm
 SHA1 0a92b4cdb402463e303b897195c9ad914767c27f lib/Jifty/Response.pm
 SHA1 669b0956c14105b4178875d6856b129b75941328 lib/Jifty/Result.pm
-SHA1 3ac318feed2accd50b61d8dc82709bd2a7882946 lib/Jifty/RightsFrom.pm
-SHA1 21247e3f5877c8954835e280b6c4eb4aa67fa5fb lib/Jifty/Schema.pm
-SHA1 522f2845a7cbea076dbc53c9ccfbadaa4de56483 lib/Jifty/Script.pm
+SHA1 d8bdfa7dfa44a99aa5ebf96b9e317ff31b54aad7 lib/Jifty/RightsFrom.pm
+SHA1 e153160eb4f058c779bf10bb823a47f7e2e0a77f lib/Jifty/Schema.pm
+SHA1 4d1aa083b1377fe5938f8e610d626995cb80d1fd lib/Jifty/Script.pm
 SHA1 f97fb67cd7db7aa2374ef4a0f1b8b9705e3ce153 lib/Jifty/Script/Action.pm
 SHA1 3aa3a449b282e05dfb312cf266572edabf2bae17 lib/Jifty/Script/Adopt.pm
-SHA1 b459c77c7f08c83b9808486cda074704c2d325c0 lib/Jifty/Script/App.pm
+SHA1 36ab3422992cd7286a01a48019d64b5b31e86b55 lib/Jifty/Script/App.pm
 SHA1 f1c1078236ad2396a7c4b98979108980e3bcd25c lib/Jifty/Script/Console.pm
 SHA1 2ae45f322b3b5904225465b85edb544ba6216eaa lib/Jifty/Script/Deps.pm
 SHA1 3c26ea2d6e25c42ecd7edae41d4ad2fc552f1451 lib/Jifty/Script/Env.pm
@@ -304,56 +338,58 @@
 SHA1 29d16baf7dbf778f6082f8852723b6ef82842ef0 lib/Jifty/Script/ModPerl2.pm
 SHA1 ae7d6a97f55a111e10cbc127d3174c48dc896afc lib/Jifty/Script/Model.pm
 SHA1 4fae01f79baf125856a69265fa9f4caeb5f858bf lib/Jifty/Script/Plugin.pm
-SHA1 3365d06f92ffe40175f2c9d7fa4175c48cd42ed9 lib/Jifty/Script/Po.pm
-SHA1 a3f347fd88bd12105ad21edf2a92dc330119adf0 lib/Jifty/Script/Schema.pm
+SHA1 163f0b9e8e01f4f4c7edf96fdbe1a27aee29b785 lib/Jifty/Script/Po.pm
+SHA1 da48a7ecb66752a367f56472d986a411b1f161be lib/Jifty/Script/Schema.pm
 SHA1 e18ad09e9078c0a9782c588398cd38541f966b5d lib/Jifty/Script/Server.pm
 SHA1 4918c842d52a7ab4de9d60da95a47a31d04d4631 lib/Jifty/Server.pm
 SHA1 b49ae221b107519b3019b3f5e5ab5b7e8c6b4332 lib/Jifty/Server/Fork.pm
-SHA1 9a8c2f2cdc88fc00baaafaa72bc3413e086c0448 lib/Jifty/Server/Prefork.pm
+SHA1 d151b0cb6c1f9b63a2671a7211c30f965b12289e lib/Jifty/Server/Prefork.pm
 SHA1 e7453a3cda290e60d5432e60e53bec8b92a91772 lib/Jifty/Server/Prefork/NetServer.pm
 SHA1 3e117a5866ca55825f4f292e95af01f98fe67edc lib/Jifty/Subs.pm
 SHA1 7b8eed10682b0e357b2049fa479e7d85bd4b1b58 lib/Jifty/Subs/Render.pm
-SHA1 9f4748146731512c6747b9eb15c6bf77d0c968be lib/Jifty/Test.pm
-SHA1 e28d417b36ca1d647a1cd5e4527465822a8ce09a lib/Jifty/Test/WWW/Mechanize.pm
+SHA1 0e48a9da0e5c6488746d92616f8cee62ecd54876 lib/Jifty/Test.pm
+SHA1 db34b059e165273d1474a858604abed5d73ef551 lib/Jifty/Test/WWW/Declare.pm
+SHA1 09c062f3089086d90490f7ce7533f010f90215db lib/Jifty/Test/WWW/Mechanize.pm
+SHA1 5011ef6d08ca86adb2f14bae1474df7ed94a50e8 lib/Jifty/Test/WWW/Selenium.pm
 SHA1 47c5840fafd56473a0e1a9228f169d3813317c13 lib/Jifty/TestServer.pm
 SHA1 5c0db87837a6a1311ec85b03f06623468afe8098 lib/Jifty/Upgrade.pm
 SHA1 fa642baf010edb939aa7682ae68b07c8a69b06b6 lib/Jifty/Upgrade/Internal.pm
-SHA1 240bfd838f0b696c43499b05218eb10e470b610d lib/Jifty/Util.pm
-SHA1 45b78364045808eeb2c895760646d910ae06c709 lib/Jifty/View.pm
-SHA1 a1040a526ea0acb07c7aa26c7d1ac6f6c19f4212 lib/Jifty/View/Declare.pm
+SHA1 a5f6803f9cb942696c2f875b0e6161b439c36a52 lib/Jifty/Util.pm
+SHA1 657bb017451f11c55873f4427b9c69fcfe10c8aa lib/Jifty/View.pm
+SHA1 5da88edb702a7ad194a13656838fff0d1459760c lib/Jifty/View/Declare.pm
 SHA1 f646c99eb386ca9ecb58e0f2184c2b153a547aaf lib/Jifty/View/Declare/BaseClass.pm
-SHA1 e58c71aeca97a483af2be912be08899de79e7d87 lib/Jifty/View/Declare/CRUD.pm
+SHA1 0f5f1f4dccecd96714a9066bf1ccf84edc607717 lib/Jifty/View/Declare/CRUD.pm
 SHA1 d2b2fa668602ba5a46b604bc4d340afb408fabd6 lib/Jifty/View/Declare/Compile.pm
 SHA1 a15bc046d04afb7f7985d65f9498d24b3bb6fdbd lib/Jifty/View/Declare/CoreTemplates.pm
-SHA1 3107a21569496a327245ca628271f2cc9bb4565e lib/Jifty/View/Declare/Handler.pm
-SHA1 bc7e7999c7d40c312eeddf1fbfef1b2bb979c37b lib/Jifty/View/Declare/Helpers.pm
-SHA1 ccbafe7217bcd46e55418c48c4dec9d82fdd71e6 lib/Jifty/View/Declare/Page.pm
-SHA1 aa6cd594be5a52a18dc40ebc01a0b8b8d943c297 lib/Jifty/View/Mason/Handler.pm
-SHA1 7c0239dd306fcdd6394771f634fb8403707e98c1 lib/Jifty/View/Static/Handler.pm
-SHA1 5182bc53778b35df9fa2d9b14c8e050016ec556b lib/Jifty/Web.pm
-SHA1 71761036222f20d7cf9f5b25e7a5d0a5ba619851 lib/Jifty/Web/Form.pm
-SHA1 f57b9359281b2a1b4334ee714da47baec4398c89 lib/Jifty/Web/Form/Clickable.pm
-SHA1 3ca9d352e41ff763c24a26d6f2780e03814cc66f lib/Jifty/Web/Form/Element.pm
-SHA1 e6b91ee72900aaa758bd2e3a46b2545b5b21133e lib/Jifty/Web/Form/Field.pm
-SHA1 d6735601c2c293b6ce0432d55bdb16fa2c5c6a68 lib/Jifty/Web/Form/Field/Button.pm
-SHA1 56fbb0c2fb940faaf086d49dcbea04825731540e lib/Jifty/Web/Form/Field/Checkbox.pm
-SHA1 02cb651fff375c73cef980ce675519f8118a6b69 lib/Jifty/Web/Form/Field/Collection.pm
-SHA1 7f64a31d99347ec482f2c95e6d06fffa6ed6d59e lib/Jifty/Web/Form/Field/Combobox.pm
+SHA1 fc44a184366a9831c80743eca696f9baea2ab93f lib/Jifty/View/Declare/Handler.pm
+SHA1 149f7757d07f7ead613781ceaf1dcd17c2d1913c lib/Jifty/View/Declare/Helpers.pm
+SHA1 a7687bce27c03cd6f8a6928fbd03b291326933ed lib/Jifty/View/Declare/Page.pm
+SHA1 a3122b829af409e32ecb3ba6c6906c5d1499cc80 lib/Jifty/View/Mason/Handler.pm
+SHA1 78b45a8f8542a4ea4ee126677a197fdbc683a7d8 lib/Jifty/View/Static/Handler.pm
+SHA1 624961e8775e5520ad53d3ac6b0dc48c76f18354 lib/Jifty/Web.pm
+SHA1 bf7ec95d6ce6c0ed7abd0d89b24d7521344a8ff3 lib/Jifty/Web/Form.pm
+SHA1 9ca1418c5fd7e539cc666cf7ab57285e49c7d0e4 lib/Jifty/Web/Form/Clickable.pm
+SHA1 26b488e8317139bbbce3ef4206ad10c59b0fc541 lib/Jifty/Web/Form/Element.pm
+SHA1 b3a69a7f5439e70b1104b015fda293c61eab62ab lib/Jifty/Web/Form/Field.pm
+SHA1 03322439e64fbaca08164b19de0d24cf0e5fdc82 lib/Jifty/Web/Form/Field/Button.pm
+SHA1 1f5e05a6cc7483112a86f9863752da7742797242 lib/Jifty/Web/Form/Field/Checkbox.pm
+SHA1 9ecf88f8cf1af8e08bc5cdcdd8357da794cca82f lib/Jifty/Web/Form/Field/Collection.pm
+SHA1 8ca1e7b6637720d0eb65c5391d52c573ad2510c7 lib/Jifty/Web/Form/Field/Combobox.pm
 SHA1 a57b8a2908b9642320f07c91fab31d8eab63c97c lib/Jifty/Web/Form/Field/Date.pm
 SHA1 d8fdc993320b9e81926181e028b5fc688ec874fe lib/Jifty/Web/Form/Field/Hidden.pm
 SHA1 4db6a90ae2668ba11eb9e8633f949ee45e56cfad lib/Jifty/Web/Form/Field/InlineButton.pm
 SHA1 1b98474c61f1065ffe76b5a9f8356e4a10afc202 lib/Jifty/Web/Form/Field/Password.pm
-SHA1 49a79b67248e87be7a3c452dc76bdc1356aaf025 lib/Jifty/Web/Form/Field/Radio.pm
-SHA1 7869f3c4a1bc2215fff467a43dec617053d8961c lib/Jifty/Web/Form/Field/ResetButton.pm
-SHA1 6e7d9e080def5cfd7fac0f6b4adf2a94b25506b1 lib/Jifty/Web/Form/Field/Select.pm
+SHA1 ceddfc29b6b45228497a0dcd33754fed669b6491 lib/Jifty/Web/Form/Field/Radio.pm
+SHA1 bbac35dafb24e72fa91643f6353796e5d856258e lib/Jifty/Web/Form/Field/ResetButton.pm
+SHA1 1957d01fa20adf8e9ee30bd44c180653c886bc11 lib/Jifty/Web/Form/Field/Select.pm
 SHA1 04507f0d805d9e8e4ef4da4d7a919f5e7d7986fe lib/Jifty/Web/Form/Field/Text.pm
-SHA1 4ceb33e9dc6a8ee744e5ef977f4ad7634795bd91 lib/Jifty/Web/Form/Field/Textarea.pm
+SHA1 b287156ff11abaf1a09739b1372d240ff1c86171 lib/Jifty/Web/Form/Field/Textarea.pm
 SHA1 20eb63da765651816b5c5c7c03bd8f8f8ca107f3 lib/Jifty/Web/Form/Field/Unrendered.pm
 SHA1 3e19292cf95afaf08e811cd9814a33d69ddfddaa lib/Jifty/Web/Form/Field/Upload.pm
-SHA1 9ad5a13ed960af0d65ab69acda01251e5efdaaa1 lib/Jifty/Web/Form/Link.pm
-SHA1 6df07c4539c2d59113989bfd2499d7c4febef426 lib/Jifty/Web/Menu.pm
-SHA1 efbc2eb67a3b148f8edfc6ae26aa767438479cac lib/Jifty/Web/PageRegion.pm
-SHA1 5039d06b6112a7e1498ab46d2c4593a6addc10ef lib/Jifty/Web/Session.pm
+SHA1 900ba3d40c9f62886b28eccb19d51772f56d5cb3 lib/Jifty/Web/Form/Link.pm
+SHA1 0a5c4adca9b3af4680a919c58ff146f70c6d1918 lib/Jifty/Web/Menu.pm
+SHA1 d407e3f85e158fa3fe86295d63fb602c4a20fae0 lib/Jifty/Web/PageRegion.pm
+SHA1 46deb18cbea37dcbbb14581965a34147d21ba091 lib/Jifty/Web/Session.pm
 SHA1 f655fb9734715ebf51fb5e9b554c02b9e4e2ac61 lib/Jifty/Web/Session/ClientSide.pm
 SHA1 d7a8f92ddbc614904ad6aa53d818b0bd5df03317 lib/Jifty/Web/Session/None.pm
 SHA1 c4de1ef964243aae5ab90d0a5a6dd9c213eb9f80 lib/Jifty/YAML.pm
@@ -961,6 +997,7 @@
 SHA1 9d339e7acebf3e929b15c1d8b4642ecbddf7f4b6 share/plugins/Jifty/Plugin/AdminUI/web/templates/__jifty/admin/fragments/list/view
 SHA1 9c6ec4bcfacc9091533b50eea7b01fe3dd81883c share/plugins/Jifty/Plugin/AdminUI/web/templates/__jifty/admin/index.html
 SHA1 99ce3e69e2201664a1363ca2535dcc51a64a1f92 share/plugins/Jifty/Plugin/AdminUI/web/templates/__jifty/admin/model/dhandler
+SHA1 07b8ea847efe863f179013a7d23087488eee03bf share/plugins/Jifty/Plugin/AutoReference/web/static/js/autoreference.js
 SHA1 0fc7fc9888d41402a0c079cb124769bd044a9f2d share/plugins/Jifty/Plugin/Chart/web/static/css/simple_bars.css
 SHA1 5a544d4b79ad280727d6a3a24a549606c69b239c share/plugins/Jifty/Plugin/Chart/web/static/flash/xmlswf/charts.swf
 SHA1 172c35bdf32dd3e7c31782a1959c7080778eb33e share/plugins/Jifty/Plugin/Chart/web/static/flash/xmlswf/charts_library/arno.swf
@@ -990,6 +1027,7 @@
 SHA1 9ffed1b5658c22190f2124fa2e6bd344ee74a1e0 share/plugins/Jifty/Plugin/Chart/web/static/js/simple_bars.js
 SHA1 15bd576afbe4cc9d44623cc8feb9b379fbe2a603 share/plugins/Jifty/Plugin/GoogleMap/web/static/css/google_map.css
 SHA1 1178d664f92ecfdd4635363fed6b53ef843cb593 share/plugins/Jifty/Plugin/GoogleMap/web/static/js/google_map.js
+SHA1 8cf05fe74c914a1c3aaa50a1a21984e630ef2acc share/plugins/Jifty/Plugin/I18N/web/static/js/loc.js
 SHA1 e1c9252b3e60673e4fa1bb1648cb18cd33139535 share/plugins/Jifty/Plugin/JQuery/web/static/js/jquery.js
 SHA1 e9813f935b17859c91b651d992cae6c90776ad14 share/plugins/Jifty/Plugin/JQuery/web/static/js/noConflict.js
 SHA1 7efa4f24d875a19035963c3955f2fc4e613b8cfe share/plugins/Jifty/Plugin/OnlineDocs/web/templates/__jifty/online_docs/autohandler
@@ -997,14 +1035,14 @@
 SHA1 58a4059cc383f792108986d2386a5e7edc81363f share/plugins/Jifty/Plugin/OnlineDocs/web/templates/__jifty/online_docs/index.html
 SHA1 37555dd5c3acfbaecfc28416e0fa21b0aa6e1d77 share/plugins/Jifty/Plugin/OnlineDocs/web/templates/__jifty/online_docs/toc.html
 SHA1 2996fff4ab04768018cfc0f617c18757e1847a7b share/po/en.po
-SHA1 63a333ff328933df4082e868ba9115b3c0fa24bd share/po/fr.po
-SHA1 88408a7350d0599fd4e7cd7f709fd918b55b6b27 share/po/ja.po
+SHA1 3540d899e92291a7d2b843e6d8187cc7b35ed87a share/po/fr.po
+SHA1 ed8f6b98f97d0e65d7d34a67afbd01ed463bf551 share/po/ja.po
 SHA1 12b260c8de9d985a8968254021cde5c7d56156b3 share/po/ru.po
 SHA1 d3f30c5bc2a7ef0fc0ea8acaaa75f03563eb16d9 share/po/zh_cn.po
-SHA1 623dbb738a8db1e79de6127fc3af2c918d2c93e6 share/po/zh_tw.po
+SHA1 71d8302cdf676b5c34ac46d6bf670c1b4735bcd6 share/po/zh_tw.po
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 share/web/static/css/app-base.css
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709 share/web/static/css/app.css
-SHA1 c566d7ab5005e82dcf2f90809ae757e07d338138 share/web/static/css/autocomplete.css
+SHA1 1a534de383a840e027c1fc9d75acdb44746e3023 share/web/static/css/autocomplete.css
 SHA1 5f569fc23eb815ee6f6d086aa6df87f6c38952d3 share/web/static/css/autohandler
 SHA1 98bd4a5fc5cdcf84794d85da8e6a91f13df875cc share/web/static/css/base.css
 SHA1 669f07ab36d148383013733d8230d49e7e7b25ec share/web/static/css/calendar.css
@@ -1015,7 +1053,7 @@
 SHA1 c7eede0c22f68e4417748bb0903b48195648f4c0 share/web/static/css/keybindings.css
 SHA1 a67bd8704c0c8e8866e01de8a98feb9788b853bd share/web/static/css/main.css
 SHA1 903069dae3de35d6a3226b8272ff317b8eebd58c share/web/static/css/nav.css
-SHA1 45861338bc40888737738521a44d4adf286b1204 share/web/static/css/notices.css
+SHA1 19b3b2e382f0c7e9619021c2e68da41a6bfc0057 share/web/static/css/notices.css
 SHA1 6af7922df30a9bcbba91de135280f35020c3de75 share/web/static/css/yui/calendar/calendar.css
 SHA1 c38a3f0ee9c3177b3b57c8a12259583937596252 share/web/static/css/yui/menu/map.gif
 SHA1 454d5a1fc8a75cfdfda8da84fcdb3ad61bc28ecf share/web/static/css/yui/menu/menu.css
@@ -1068,7 +1106,7 @@
 SHA1 4553f3cb184b09228ed4362898e9d30200a2a585 share/web/static/js/formatDate.js
 SHA1 a1d2c6292d656c275383b97aad6ca913b8a1b031 share/web/static/js/halo.js
 SHA1 08b20563e958e72c3e8a221d91614c412bdd068b share/web/static/js/iepngfix.htc
-SHA1 762552c69eda7c920f366e0fdabd5010399cc1c2 share/web/static/js/jifty.js
+SHA1 b0da2c62db377608f91b5ed075811399eb44da75 share/web/static/js/jifty.js
 SHA1 29fe34f11192976f1a388562188b1eb9af7f4497 share/web/static/js/jifty_smoothscroll.js
 SHA1 8723bf251531e79ab109ea0d3fb2187a8dac8cb6 share/web/static/js/jifty_subs.js
 SHA1 1a4ccf6b5d376984d91c439e1642bd2b7fb11115 share/web/static/js/jifty_utils.js
@@ -1081,12 +1119,12 @@
 SHA1 becdf6868ec4aec2dc93c8c33b0713d1c4f4eb34 share/web/static/js/jsan/Upgrade/Function/apply.js
 SHA1 f15b0364f99d2e4c1af795c82883f89b9eaca9b2 share/web/static/js/json.js
 SHA1 7a09ac75f9140b7faf2f3b0e97493c78997798f6 share/web/static/js/key_bindings.js
-SHA1 986a63bc533f6fa99c9b0f0226a14f9871b94ce5 share/web/static/js/prototype.js
-SHA1 a1048deeafbc76659e54eb77c0e51b6b79cade19 share/web/static/js/rico.js
+SHA1 a25ea3a28e4aa63eb625a3a8b971d89a82825466 share/web/static/js/prototype.js
+SHA1 ef4db83a2883d18878ae2f92e64cbcb21a053a94 share/web/static/js/rico.js
 SHA1 164bc59cf75fe943edc80da65b19246fc9b9643e share/web/static/js/scriptaculous/builder.js
 SHA1 afa5b63db51fc4c3c4ff2535d3af5fe5b00add19 share/web/static/js/scriptaculous/controls.js
 SHA1 28f001d3c48395daf0de22876a70d918db3a461e share/web/static/js/scriptaculous/dragdrop.js
-SHA1 21ce51daa693e3716678ac4190369b499b35e8de share/web/static/js/scriptaculous/effects.js
+SHA1 136d82eaaca78f577292ee5df7886675cf897022 share/web/static/js/scriptaculous/effects.js
 SHA1 914db330c7fe585dfeddce713558f04328fb51db share/web/static/js/scriptaculous/scriptaculous.js
 SHA1 cc2e31820eed69ae87b1b2befa50e8c4a8519342 share/web/static/js/scriptaculous/slider.js
 SHA1 6b42a40cac7d45f9fd6665e18c4e494704eff9e3 share/web/static/js/scriptaculous/unittest.js
@@ -1128,9 +1166,9 @@
 SHA1 1bd17a07884f71740a048c41b67ac9b06915bf76 share/web/templates/index.html
 SHA1 c118e782947f715afec7b5cffa4ebc413e990c12 share/web/transform_templates
 SHA1 7f9dae91a9bfc2743eec1d7aaf78e16fc9f1baba t/00-load.t
-SHA1 7922f3d21505c630e6a694c7b3cc07e6bbf246f7 t/01-dependencies.t
+SHA1 4d2af70e8612e39f60cee6b9477d5da99d15fdd6 t/01-dependencies.t
 SHA1 777e40e7d3c591c21883d0ed880df8712dd50ff3 t/01-test-mechanize.t
-SHA1 6fdfbd2d5db06e2f8ed23d2c551b8ac3f6e9cecf t/01-test-web.t
+SHA1 ade22974f54a6e0991e14be587be0c9797e72ed5 t/01-test-web.t
 SHA1 95fe956e7bae756a7bc25e65a6761ee0c64981e7 t/01-version_checks.t
 SHA1 4fa0e0143339298278c5e22a58236c6b71555508 t/02-connect.t
 SHA1 46221e3b0272c3ef2f5ce0032239dc3aba1d2e04 t/03-form-protocol.t
@@ -1148,7 +1186,7 @@
 SHA1 d571f6fae9d1a33060fda8c89951492a02b1af01 t/11-config-files.t
 SHA1 bd4520e6f2bfdabc6dba2d27e0cb6d33453f82e7 t/12-param-schema.t
 SHA1 d4b169132cc2aaaa2ff9c38b2e7009fc38918567 t/13-sessions.t
-SHA1 59c44900b1cb957d262f96363ceff21b46e0d598 t/99-pod-coverage.t
+SHA1 cf5b3950070fda63ba1b497f7d89dd6c36ae9c93 t/99-pod-coverage.t
 SHA1 bb0da54f2b3f2d7955baa41ee458cb3d1887f475 t/99-pod.t
 SHA1 a7dc1f376cac630ea28d2965e561469deb951cc7 t/Continuations/bin/jifty
 SHA1 adbc53cbd328b4d49d3336586fe8f7b7124da970 t/Continuations/lib/Continuations/Action/CrossBridge.pm
@@ -1189,15 +1227,41 @@
 SHA1 94c39dbef547367bb6e6da1b9223cf1608ed748e t/TestApp-Plugin-Chart/lib/TestApp/Plugin/Chart/View.pm
 SHA1 ff856d51a878c5ff67f828c67088af5ca9248591 t/TestApp-Plugin-Chart/t/chart.t
 SHA1 8912276067f1707ad2a05677bd54c2013bc49ff1 t/TestApp-Plugin-Chart/t/gd_graph.t
+SHA1 711894dd5ab6923eb725582e7cd739c2ca5c403e t/TestApp-Plugin-CompressedCSSandJS/Makefile.PL
+SHA1 c1ff9ff7f2a88bc4306b3866b6b80fb9aa8e8423 t/TestApp-Plugin-CompressedCSSandJS/bin/jifty
+SHA1 b51eb68ba760a5f48db573d44eb7588a57f02a57 t/TestApp-Plugin-CompressedCSSandJS/etc/config.yml
+SHA1 7056be7bdbb7d6eefdcf5581e60c48b5b588e567 t/TestApp-Plugin-CompressedCSSandJS/t/css.t
 SHA1 711894dd5ab6923eb725582e7cd739c2ca5c403e t/TestApp-Plugin-JQuery/Makefile.PL
 SHA1 c1ff9ff7f2a88bc4306b3866b6b80fb9aa8e8423 t/TestApp-Plugin-JQuery/bin/jifty
-SHA1 430cca6c8488b9f2f5b061bfcdadbd5f33db3b82 t/TestApp-Plugin-JQuery/etc/config.yml
+SHA1 f2b2073c99fd411f2b43db5cbcad29c1d6eafc74 t/TestApp-Plugin-JQuery/etc/config.yml
 SHA1 93b414ca2f8f633893a5bc460c136754042772ac t/TestApp-Plugin-JQuery/t/jquery.t
 SHA1 543e899febfce9d8b9c644d557ef3af98f6ad89e t/TestApp-Plugin-News/Makefile.PL
 SHA1 f7f44f9a7337def0c97f981073e3ed970851d9ae t/TestApp-Plugin-News/bin/jifty
 SHA1 47529b82ece48a5943eac7c1ab8b2de5c94f8795 t/TestApp-Plugin-News/etc/config.yml
 SHA1 f69a79e81e98c9c6a99c74cedd9b060b6618970a t/TestApp-Plugin-News/lib/TestApp/Plugin/News/Model/News.pm
 SHA1 53c30add7fff4ebeedf1b6b2ee8474c3bc34efd6 t/TestApp-Plugin-News/lib/TestApp/Plugin/News/View.pm
+SHA1 e30ec0746a5a0681129d963a55b36930c7f0b0c3 t/TestApp-Plugin-OAuth/Makefile.PL
+SHA1 c1ff9ff7f2a88bc4306b3866b6b80fb9aa8e8423 t/TestApp-Plugin-OAuth/bin/jifty
+SHA1 42e1e283dabedf44d2deae64ee46bab06c7f8643 t/TestApp-Plugin-OAuth/etc/config.yml
+SHA1 dc7624e59325207bbfc5205b6f46a840993b511a t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Dispatcher.pm
+SHA1 8a86bdcafd192866d1b713c2d8f5a3d7bf4ac5d4 t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Model/User.pm
+SHA1 027ce32b06ae148269ae68aa6c54d8ca3cef6d05 t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Test.pm
+SHA1 7a121b004811014f5674d55da450995680b23052 t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/View.pm
+SHA1 fceb03fe7a34fdff50a5c808364cec21e2a5cadf t/TestApp-Plugin-OAuth/t/00-test-setup.t
+SHA1 fdd04ef4b775843c808e4947805daab4eb213a86 t/TestApp-Plugin-OAuth/t/01-basic.t
+SHA1 1e4022b12b882130ecfca27d61a75e0ccfe7fca8 t/TestApp-Plugin-OAuth/t/02-request-token.t
+SHA1 2b118556b4d823f1e9a88eb501332e2ab0bda461 t/TestApp-Plugin-OAuth/t/03-authorize.t
+SHA1 72aadc20063aac4bb46d0f30617de4f0ece62772 t/TestApp-Plugin-OAuth/t/04-access-token.t
+SHA1 6f007457f45f1281cfa173562097ab8586a9fb52 t/TestApp-Plugin-OAuth/t/05-protected-resource.t
+SHA1 e38a2d7034e474f2efb95efdb01afdbb1c675d7d t/TestApp-Plugin-OAuth/t/id_rsa
+SHA1 73fd51a00ea7b52b0feffbc2e11d8fefc918b814 t/TestApp-Plugin-OAuth/t/id_rsa.pub
+SHA1 726df49c955a3ed0fe0a511f462b14408b3e34a1 t/TestApp-Plugin-OnClick/Makefile.PL
+SHA1 c1ff9ff7f2a88bc4306b3866b6b80fb9aa8e8423 t/TestApp-Plugin-OnClick/bin/jifty
+SHA1 750bc31fc1747ca292d3f2d6b82600eac85ee7ef t/TestApp-Plugin-OnClick/etc/config.yml
+SHA1 09c2f8647e14e49e922b955c194102070597c2d1 t/TestApp-Plugin-OnClick/share/web/templates/content.html
+SHA1 358cb997ccc4f7db96e5a3bb710a25b1affd8823 t/TestApp-Plugin-OnClick/share/web/templates/content1.html
+SHA1 dfe47fe40442840e7b738c7587a55c8324bff55d t/TestApp-Plugin-OnClick/share/web/templates/onclick.html
+SHA1 2a6feee9c6f26290b11f5293702dc38f87e86139 t/TestApp-Plugin-OnClick/t/onclick.t
 SHA1 5151dae3d7ac5f80dcfaf39fdeea0157af85f189 t/TestApp-Plugin-PasswordAuth/Makefile.PL
 SHA1 f7f44f9a7337def0c97f981073e3ed970851d9ae t/TestApp-Plugin-PasswordAuth/bin/jifty
 SHA1 2ad861771e8cc20e90a5820cb4d3d0837a4fc047 t/TestApp-Plugin-PasswordAuth/etc/config.yml
@@ -1222,14 +1286,24 @@
 SHA1 3b7b51b4428dcbf0b9b1d55c39fd139a3ee4868a t/TestApp-Plugin-REST/t/00-prototype.t
 SHA1 b6c65e7f47ff136c7d370b85cf01f27537dd81ff t/TestApp-Plugin-REST/t/01-config.t
 SHA1 daa99f581a0b42976ce7ae4fe8c3f4d79d799827 t/TestApp-Plugin-REST/t/02-basic-use.t
+SHA1 711894dd5ab6923eb725582e7cd739c2ca5c403e t/TestApp-Plugin-SinglePage/Makefile.PL
+SHA1 c1ff9ff7f2a88bc4306b3866b6b80fb9aa8e8423 t/TestApp-Plugin-SinglePage/bin/jifty
+SHA1 92ac1f7352e3e68ec39ef71e2c829f2cddec59af t/TestApp-Plugin-SinglePage/etc/config.yml
+SHA1 65259a1e0c8a8c45b36800da0d34d3f370f1f0c3 t/TestApp-Plugin-SinglePage/lib/TestApp/Plugin/SinglePage/Model/User.pm
+SHA1 238ee5edab29f5d6ac7d79be59c524c801028b87 t/TestApp-Plugin-SinglePage/lib/TestApp/Plugin/SinglePage/View.pm
 SHA1 a7dc1f376cac630ea28d2965e561469deb951cc7 t/TestApp/bin/jifty
+SHA1 9de42ecbfd32964a392a2b1dbe39c077d8145737 t/TestApp/etc/config.yml
+SHA1 66da809653f3152522ce5760f0894a86624b2394 t/TestApp/etc/site_config.yml
 SHA1 f9a9321b803e4f248ecdd86fd71613164c01bd86 t/TestApp/lib/TestApp/Action/DoSomething.pm
 SHA1 f93118ca17be86a7c171ee47864d7149baf7344c t/TestApp/lib/TestApp/Action/DoSomethingElse.pm
+SHA1 05114360ab9678da24bbc0ad68562756d6300681 t/TestApp/lib/TestApp/Action/SayHi.pm
 SHA1 6e27f855429d18181d6c5a0de6b83492fa5c6219 t/TestApp/lib/TestApp/CurrentUser.pm
 SHA1 0ff8efa1a274a4e0db2256a7231886ab27f3eb18 t/TestApp/lib/TestApp/Dispatcher.pm
-SHA1 f1465b5bab96659951965e968b580d1c7c218485 t/TestApp/lib/TestApp/Model/User.pm
+SHA1 01cc97136094c4dcaa0b00f7b89ea679f35e752d t/TestApp/lib/TestApp/Model/OtherThingy.pm
+SHA1 60e2eed33da6b06ea32506cc17229f8ed3c4d482 t/TestApp/lib/TestApp/Model/Thingy.pm
+SHA1 24b88bd57bed05b8ef19edd5ac42773f934b8d23 t/TestApp/lib/TestApp/Model/User.pm
 SHA1 b2d3474949dae7c171157e8697dbb208ef3805f8 t/TestApp/lib/TestApp/Upgrade.pm
-SHA1 ac8b1d259e0c4115ff4f0f11910fd55d80a4a6c8 t/TestApp/lib/TestApp/View.pm
+SHA1 0ebc732808b445f31c4dd54dec81d52329026bbf t/TestApp/lib/TestApp/View.pm
 SHA1 f8be83226541599e4543484128fd209dfc664ff6 t/TestApp/lib/TestApp/View/base.pm
 SHA1 e376d22f0db953b5c9ede1e4e07660fe94390bf5 t/TestApp/lib/TestApp/View/instance.pm
 SHA1 67f41db40d62b81d71cb60c542695e0d7e6d393d t/TestApp/share/web/static/images/pony.jpg
@@ -1250,7 +1324,7 @@
 SHA1 cc7e1174609f5ae92b441c4ecf7a4734cf5a9436 t/TestApp/share/web/templates/regions/long
 SHA1 f7b091879df762cccacaf827fbadc1ad4b6294b1 t/TestApp/share/web/templates/regions/short
 SHA1 9a50bb56338896f9cd50b7098a9e28397ad28a34 t/TestApp/share/web/templates/somedir/dhandler
-SHA1 350ab77f7c18d826ed91eaf251f3f80ebd9605ba t/TestApp/t/00-model-User.t
+SHA1 06ee53664c6d4dffb42190c5157dd682607f0818 t/TestApp/t/00-model-User.t
 SHA1 3b7b51b4428dcbf0b9b1d55c39fd139a3ee4868a t/TestApp/t/00-prototype.t
 SHA1 94a1fe86cc34fbdce9087f30a69a859063ca8714 t/TestApp/t/01-config.t
 SHA1 77466bc82d8f48bb7890bcf7900749b73b2a9f97 t/TestApp/t/02-dispatch-http.t
@@ -1267,27 +1341,36 @@
 SHA1 3c28d88a25d4f7ed0edf2450d7b4ab7577f04dbe t/TestApp/t/08-notifications.t
 SHA1 3e648a4acabeb7a137f28a01698b8c1853a65aa1 t/TestApp/t/09-redirect.t
 SHA1 55ba141d6c73a6dfa7ccccb6cb9f32253ed8decb t/TestApp/t/10-compress.t
-SHA1 5d013aae657f16eb75b6c929fc4fcd66c186f2b6 t/TestApp/t/11-current_user.t
+SHA1 1bbc046a6c6115b67087149fe5c7358c6bfa7ab9 t/TestApp/t/11-current_user.t
 SHA1 019605c6e627bf65ee3d2ed1b8d2b34fc8e10853 t/TestApp/t/12-search.t
 SHA1 1c80b396277bf78134c2aa3de9e0f375e7efef75 t/TestApp/t/13-page-regions.t
-SHA1 b57b4cf6832d53a3777fe65e56f9ae076f474b0a t/TestApp/t/14-template-paths.t
+SHA1 b2e93a99969307d1cec54016f6f23c3f15c66af4 t/TestApp/t/14-template-paths.t
 SHA1 af490a8e49140d63e12feb96a8544bc1ea4fe925 t/TestApp/t/15-template-subclass.t
 SHA1 10286b19e32167d07ad59c927dcbe46159edb184 t/TestApp/t/16-template-region.t
 SHA1 e4f33eb967231751ce043a1be3cdbf526671127c t/TestApp/t/17-template-region-internal-redirect.t
-SHA1 cbf28439ca71f373a547db875732a0b2e5ce9a7f t/TestApp/t/before_access.t
+SHA1 338a0f857d73bbf76ca6490e1dc276813767e603 t/TestApp/t/18-test-www-declare.t
+SHA1 23c8686edc138faa7faa1db3eb8c460dd26cd191 t/TestApp/t/19-rightsfrom.t
+SHA1 5108b2617b04b1f903534f8cad9c4f307bbe8337 t/TestApp/t/before_access.t
 SHA1 69401ad0579fa743f087731536229d2806dd1d6a t/TestApp/t/config-Cachable
 SHA1 576e73eedda16d331b018da82b36d01510060422 t/TestApp/t/config-Record
+SHA1 eb2cf455fc2123ee3320f9fdcbc5a68fc1b8286f t/TestApp/t/config/01-basic.t
+SHA1 7ce5f003d1cda10b6cfcec2db107f08d9389339c t/TestApp/t/config/02-individual.t
+SHA1 9bd0b9d64c8bb22183beba7dd091c8f60f1718ee t/TestApp/t/config/02-individual.t-config.yml
+SHA1 91443737eab5ac3f4f51d1925917acd3b190fd3d t/TestApp/t/config/03-nosubtest.t
+SHA1 b9e3bfb1b277c9c61890cc292d711e61f5d5f1e8 t/TestApp/t/config/test_config.yml
+SHA1 e0d433ff3707e84d370b9b3a67306c66275ebe41 t/TestApp/t/crud.t
 SHA1 6c16e68284cc30d81eba755c6986675690b78f77 t/TestApp/t/i18n-standalone.t
 SHA1 30274351a6eb9342daef843ffb8a2aafee38afb4 t/TestApp/t/instance_id.t
 SHA1 ee548850452b377e08f36a9269c1b8f7911bdb2d t/TestApp/t/regex_meta_in_path_info.t
+SHA1 0270868131aad30c20ef0be68a43393c088cc902 t/TestApp/t/test_config.yml
 SHA1 f502e4937629f7525cf90cf982cadd29bc60ed5a t/TestApp/t/upgrade.t
 SHA1 a415b718785f367c0c7c4e8f72b33613f5dbce44 t/TestApp/t/use_mason_wrapper.t
 SHA1 ca61102870f9c092374d7d74c19174ce601d80e7 t/clientside/td.t
-SHA1 c8fb21f31b593627b38129ee9dd41eaf9c556ced t/lib/Jifty/SubTest.pm
+SHA1 5c0b3c689031ace0031a3520cca976b852a622ed t/lib/Jifty/SubTest.pm
 -----BEGIN PGP SIGNATURE-----
-Version: GnuPG v1.4.3 (Darwin)
+Version: GnuPG v1.4.7 (Darwin)
 
-iD8DBQFGzlvEEi9d9xCOQEYRAqPEAKCOzwFVQ+R1uimjKfJQgAiOh69VAwCfUI6C
-XxD2rfrthtS5PgsTW0eahl4=
-=f54M
+iD8DBQFHTzf8sxfQtHhyRPoRAtMwAJ40MXZ6kf+ovoX6tMgjllppXjrwJwCeKZX9
+riVXS6LnAwGUNZThrnydlpo=
+=8vIq
 -----END PGP SIGNATURE-----

Modified: jifty/branches/virtual-models/debian/control
==============================================================================
--- jifty/branches/virtual-models/debian/control	(original)
+++ jifty/branches/virtual-models/debian/control	Sat Dec  1 17:17:34 2007
@@ -1,11 +1,14 @@
 Source: jifty
 Section: web
 Priority: optional
-Maintainer: Bart Bunting <bart at debian.org>
+Maintainer: Yves Agostini <agostini at univ-metz.fr>
 Build-Depends-Indep: debhelper (>> 5), libmodule-corelist-perl,
  libdevel-cover-perl, libmodule-scandeps-perl, libpod-simple-perl,
  libtest-base-perl, libtest-www-mechanize-perl (>> 1.04),
- libtest-pod-coverage-perl
+ libtest-pod-coverage-perl, libwww-mechanize-perl (>> 1.12),
+ libclass-accessor-named-perl, 
+ libmodule-install-perl, libtest-mockobject-perl,
+ libtest-mockmodule-perl, libtest-www-declare-perl
 
 Package: jifty
 Section: perl
@@ -47,9 +50,9 @@
 Section: perl
 Architecture: all
 Depends: ${perl:Depends}, perl (>> 5.8.3),
- libapp-cli-perl (>> 0.03), libcache-cache-perl,
+ libapp-cli-perl (>> 0.03), libclone-perl (>> 0.0.27), libcache-cache-perl,
  libcalendar-simple-perl, libclass-accessor-perl,
- libclass-container-perl, libclass-data-inheritable-perl,
+ libclass-container-perl, libclass-data-inheritable-perl, libcss-squish-perl (>> 0.0.7),
  perl-modules, libcgi-cookie-splitter-perl, libcgi-simple-perl,
  libcrypt-cbc-perl, libcrypt-rijndael-perl,
  libcompress-zlib-perl, libcss-squish-perl (>> 0.05), 
@@ -58,8 +61,8 @@
  libdatetime-perl, libdatetime-format-builder-perl, 
  libdate-manip-perl, libemail-folder-perl,
  libemail-messageid-perl, libemail-mime-perl, libemail-mime-encodings-perl, libemail-mime-perl, libemail-mime-encodings-perl, libemail-mime-contenttype-perl, libemail-simple-perl, libemail-mime-modifier-perl, libemail-mime-creator-perl, libemail-mime-createhtml-perl,
- libemail-localdelivery-perl, 
- libemail-send-perl (>> 1.99_01), 
+ libemail-localdelivery-perl (>> 0.217), 
+ libemail-send-perl (>> 2.003), 
  libemail-simple-creator-perl, libexporter-lite-perl,
  libextutils-command-perl, 
  libfile-find-rule-perl, libfile-mmagic-perl,
@@ -73,6 +76,7 @@
  libmodule-corelist-perl, libmodule-refresh-perl,
  libmodule-scandeps-perl, libobject-declare-perl (>> 0.22),
  libparams-validate-perl, libscalar-defer-perl (>> 0.10),
+ libpadwalker-perl,
  libstring-koremutake-perl, libsql-reservedwords-perl,
  libtemplate-declare-perl (>> 0.26), 
  libtest-base-perl, libtest-log4perl-perl, 
@@ -83,13 +87,12 @@
  libpod-simple-perl, 
  libsub-exporter-perl, libcache-memcached-perl, liblog-dispatch-perl
 Recommends: libmodule-install-perl,
- libtest-pod-coverage-perl, libtest-www-mechanize-perl (>> 1.04), libwww-mechanize-perl (>> 1.12),
- libclass-accessor-named-perl, libdevel-cover-perl,
- libmodule-install-perl, libpar-dist-fromcpan-perl, libtest-mockobject-perl,
- libtest-mockmodule-perl,
  libcgi-fast-perl, libapache-mod-fastcgi,
- libjifty-plugin-editinplace-perl, libjifty-plugin-login-perl,
- libimage-info-perl, libchart-perl, libgd-gd2-perl, libgd-graph-perl
+ libjifty-plugin-login-perl, libpar-dist-fromcpan-perl,
+ libjifty-plugin-authentication-cas-perl, libjifty-plugin-authentication-ldap-perl,
+ libjifty-plugin-openid-perl, libjifty-plugin-oauth-perl,
+ libjifty-plugin-chart-perl, libjifty-plugin-wikitoolbar-perl,
+ libjifty-plugin-editinplace-perl
 Description: Jifty perl libraries
  Yet another web framework.
  .
@@ -122,3 +125,35 @@
  .
  This package provides the Jifty perl libraries.
 
+Package: libjifty-plugin-authentication-cas-perl
+Section: perl
+Architecture: all
+Depends: ${perl:Depends}, perl (>> 5.8.3), jifty, libauthen-cas-client-perl
+Description: Jifty plugin for Jasig CAS authentication
+
+Package: libjifty-plugin-authentication-ldap-perl
+Section: perl
+Architecture: all
+Depends: ${perl:Depends}, perl (>> 5.8.3), jifty, libnet-ldap-perl
+Description: Jifty plugin for Jasig CAS authentication
+
+Package: libjifty-plugin-openid-perl
+Section: perl
+Architecture: all
+Depends: ${perl:Depends}, perl (>> 5.8.3), jifty, liburi-fetch-perl, libcrypt-dh-perl, 
+  libnet-openid-consumer-perl, libcache-cache-perl, liblwpx-paranoidagent-perl
+Description: Jifty plugin for OpenID authentication
+
+Package: libjifty-plugin-chart-perl
+Section: perl
+Architecture: all
+Depends: ${perl:Depends}, perl (>> 5.8.3), jifty, libchart-perl,libgd-graph-perl, libgd-gd2-perl,
+ libimage-info-perl, libxml-simple-perl
+Description: Jifty plugin for Chart
+
+Package: libjifty-plugin-oauth-perl
+Section: perl
+Architecture: all
+Depends: ${perl:Depends}, perl (>> 5.8.3), jifty, libcrypt-openssl-bignum-perl, libdigest-hmac-perl,
+ libnet-oauth-perl
+Description: Jifty plugin for OAuth authentication

Modified: jifty/branches/virtual-models/lib/Jifty.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty.pm	Sat Dec  1 17:17:34 2007
@@ -5,13 +5,15 @@
 use IPC::PubSub 0.22;
 use Data::UUID;
 use encoding 'utf8';
+use Class::Trigger;
+
 BEGIN { 
     # Work around the fact that Time::Local caches TZ on first require
     local $ENV{'TZ'} = "GMT";
     require Time::Local;
 
     # Declare early to make sure Jifty::Record::schema_version works
-    $Jifty::VERSION = '0.70824';
+    $Jifty::VERSION = '0.71129';
 }
 
 =head1 NAME
@@ -241,6 +243,10 @@
     # Run the App::start() method if it exists for app-specific initialization
     $app->start()
         if $app->can('start');
+
+    # For plugins that want all the above initialization, but want to run before
+    # we begin serving requests
+    Jifty->call_trigger('post_init');
 }
 
 # Explicitly destroy the classloader; if this happens during global

Modified: jifty/branches/virtual-models/lib/Jifty/ClassLoader.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/ClassLoader.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/ClassLoader.pm	Sat Dec  1 17:17:34 2007
@@ -109,6 +109,12 @@
 
 An empty class that descends from L<Jifty::Handle> is created.
 
+=item I<Application>::Model::I<Something>
+
+If C<I<Application>::Plugin::I<Plugin>::Model::I<Something>> exists and is a model class, then it creates a subclass of it named C<I<Application>::Model::I<Something>> for the local application.
+
+This allows an application to customize a model provided by a subclass (or choose not to). Plugins should be written to use the application's version.
+
 =item I<Application>::Model::I<Something>Collection
 
 If C<I<Application>::Model::I<Something>> is a valid model class, then
@@ -177,7 +183,7 @@
                 . "use Jifty::View::Declare -base; sub _autogenerated { 1 };\n"
             );
     } 
-    
+
     # Autogenerate the Collection class for a Model
     elsif ( $module =~ /^(?:$base)::Model::([^\.]+)Collection$/ ) {
         return $self->return_class(
@@ -215,14 +221,15 @@
         Jifty::Util->_require( module => $modelclass, quiet => 1);
 
         # Don't generate the action unless it really is a model
-        return undef unless eval { $modelclass->isa('Jifty::Record') };
+        if ( eval { $modelclass->isa('Jifty::Record') } ) {
 
-        return $self->return_class(
+            return $self->return_class(
                   "package $module;\n"
                 . "use base qw/$base\::Action::Record::$1/;\n"
                 . "sub record_class { '$modelclass' };\n"
                 . "sub _autogenerated { 1 };\n"
             );
+        }
 
     }
 
@@ -233,11 +240,11 @@
     # type of notification or action.
     #
     # This allows the application to customize what happens on a plugin action
-    # or customize the email notification sent by a plugin. 
+    # or model or customize the email notification sent by a plugin. 
     #
     # However, this depends on the plugin being well-behaved and always using
-    # the application version of actions and notifications rather than trying
-    # to use the plugin class directly.
+    # the application version of actions, models, and notifications rather than
+    # trying to use the plugin class directly.
     #
     # Of course, if the class loader finds such a case, then the application
     # has not chosen to override it and we're generating the empty stub to take
@@ -250,7 +257,7 @@
     # Plugin::Model::Thing instead.
     
     # Requesting an application override of a plugin action or notification?
-    if ( $module =~ /^(?:$base)::(Action|Notification)::(.*)$/x and not grep {$_ eq $base} map {ref} Jifty->plugins ) {
+    if ( $module =~ /^(?:$base)::(Action|Model|Notification)::(.*)$/x and not grep {$_ eq $base} map {ref} Jifty->plugins ) {
         my $type = $1;
         my $item = $2;
 
@@ -259,12 +266,23 @@
             next if ($plugin eq $base);
             my $class = $plugin."::".$type."::".$item;
 
-            # Found it! Generate the empty stub.
+            # Found it!
             if (Jifty::Util->try_to_require($class) ) {
+
+                # Models need to load their other stuff, but to prevent deep
+                # recursion, this must happen after the class is compiled.
+                my $module_suffix = '';
+                $module_suffix = "Jifty->class_loader"
+                               . "->_require_model_related_classes("
+                               . "'$module');\n"
+                    if $type eq 'Model';
+
+                # Generate the empty stub
                 return $self->return_class(
                         "package $module;\n"
                         . "use base qw/$class/;\n"
                         . "sub _autogenerated { 1 };\n"
+                        . $module_suffix
                     );
             }
         }
@@ -340,6 +358,7 @@
     for my $full ($self->models) {
         $self->_require_model_related_classes($full);
     }
+    $_->finalize_triggers for grep {$_->can('finalize_triggers')} $self->models;
 }
 
 # This class helps Jifty::ClassLoader::require() load each model, the model's

Modified: jifty/branches/virtual-models/lib/Jifty/Config.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Config.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Config.pm	Sat Dec  1 17:17:34 2007
@@ -289,6 +289,7 @@
                 Backend => 'Memcached',
             },
             Database         => {
+                AutoUpgrade => 1,
                 Database =>  $db_name,
                 Driver   => "SQLite",
                 Host     => "localhost",

Modified: jifty/branches/virtual-models/lib/Jifty/Everything.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Everything.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Everything.pm	Sat Dec  1 17:17:34 2007
@@ -7,6 +7,14 @@
 
 Jifty::Everything - Load all of the important Jifty modules at once.
 
+=head1 DESCRIPTION
+
+This package is loaded very early in the processof loading Jifty to bring in all of the wonderful goodies that make up Jifty. If you use L<JIfty>:
+
+  use Jifty;
+
+you use this package, so you should not need to use it yourself in most circumstances.
+
 =cut
 
 use Cwd ();
@@ -81,4 +89,19 @@
 #Jifty::Module::Pluggable->import(search_path => ['Jifty::Web::Form::Field'], require     => 1, except      => qr/\.#/);
 #__PACKAGE__->plugins;
 
+# load commands defined in Jifty/Plugin/*/Command/*.pm
+Jifty::Module::Pluggable->import(search_path => ['Jifty::Plugin'], file_regex => qr{/Command/[^/]+}, require => 1);
+__PACKAGE__->plugins;
+
+=head1 SEE ALSO
+
+L<Jifty>
+
+=head1 LICENSE
+
+Jifty is Copyright 2005-2007 Best Practical Solutions, LLC.
+Jifty is distributed under the same terms as Perl itself.
+
+=cut
+
 1;

Modified: jifty/branches/virtual-models/lib/Jifty/Filter/DateTime.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Filter/DateTime.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Filter/DateTime.pm	Sat Dec  1 17:17:34 2007
@@ -6,6 +6,15 @@
 Jifty::Filter::DateTime -- A Jifty::DBI filter to work with
                           Jifty::DateTime objects
 
+=head1 SYNOPSIS
+
+   # use it with Jifty::DBI::Filter::Date or J::D::F::DateTime
+    column created =>
+      type is 'timestamp',
+      filters are qw( Jifty::Filter::DateTime Jifty::DBI::Filter::DateTime),
+      label is 'Created',
+      is immutable;
+
 =head1 DESCRIPTION
 
 Jifty::Filter::DateTime promotes DateTime objects to Jifty::DateTime
@@ -62,6 +71,11 @@
 L<Jifty::DBI::Filter::Date>, L<Jifty::DBI::Filter::DateTime>,
 L<Jifty::DateTime>
 
+=head1 LICENSE
+
+Jifty is Copyright 2005-2007 Best Practical Solutions, LLC.
+Jifty is distributed under the same terms as Perl itself.
+
 =cut
 
 1;

Modified: jifty/branches/virtual-models/lib/Jifty/Handle.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Handle.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Handle.pm	Sat Dec  1 17:17:34 2007
@@ -98,7 +98,7 @@
 
     my %lc_db_config;
     # Skip the non-dsn keys, but not anything else
-    for (grep {!/^checkschema|version|forwardcompatible|recordbaseclass|attributes$/i} keys %db_config) {
+    for (grep {!/^autoupgrade|checkschema|version|forwardcompatible|recordbaseclass|attributes$/i} keys %db_config) {
         $lc_db_config{lc($_)} = $db_config{$_};
     }
     $self->SUPER::connect( %lc_db_config , %args);
@@ -121,7 +121,9 @@
 =cut
 
 sub check_schema_version {
+    my $self = shift;
     require Jifty::Model::Metadata;
+            my $autoup = delete Jifty->config->framework('Database')->{'AutoUpgrade'};
 
     # Application db version check
     {
@@ -155,10 +157,16 @@
         unless (version->new($appv) == version->new($dbv)) {
             # if app version is older than db version, but we are still compatible
             my $compat = delete Jifty->config->framework('Database')->{'ForwardCompatible'} || $appv;
-            die
-            "Application schema version in database ($dbv) doesn't match application schema version ($appv)\n"
-            . "Please run `bin/jifty schema --setup` to upgrade the database.\n"
-                if version->new($appv) > version->new($dbv) || version->new($compat) < version->new($dbv);
+            if (version->new($appv) > version->new($dbv) || version->new($compat) < version->new($dbv)) {
+            warn "Application schema version in database ($dbv) doesn't match application schema version ($appv)\n";
+            if( $autoup 
+            ) {
+                warn "Automatically upgrading your database to match the current application schema";
+                $self->_upgrade_schema();
+            } else {
+                 die "Please run `bin/jifty schema --setup` to upgrade the database.\n";
+             }
+            }
         }
     }
 
@@ -172,10 +180,15 @@
             = version->new( Jifty::Model::Metadata->load("jifty_db_version")
                 || '0.60426' );
         my $appv = version->new($Jifty::VERSION);
-        die
-            "Internal jifty schema version in database ($dbv) doesn't match running jifty version ($appv)\n"
-            . "Please run `bin/jifty schema --setup` to upgrade the database.\n"
-            unless $appv == $dbv;
+            unless ( $appv == $dbv ) {
+           warn "Internal jifty schema version in database ($dbv) doesn't match running jifty version ($appv)\n";
+            if( $autoup) {
+                warn "Automatically upgrading your database to match the current Jifty schema\n";
+                $self->_upgrade_schema;
+            } else {
+        die "Please run `bin/jifty schema --setup` to upgrade the database.\n"
+            }
+        };
     }
 
 }
@@ -443,6 +456,15 @@
     }
 }
 
+sub _upgrade_schema {
+    my $self = shift;
+    my $hack = {};
+    require Jifty::Script::Schema;
+    bless $hack, "Jifty::Script::Schema";
+    $hack->run_upgrades;
+
+}
+
 =head1 AUTHOR
 
 Various folks at BestPractical Solutions, LLC.

Modified: jifty/branches/virtual-models/lib/Jifty/Handler.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Handler.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Handler.pm	Sat Dec  1 17:17:34 2007
@@ -218,14 +218,13 @@
         for ( Jifty->plugins ) {
             $_->new_request;
         }
-        Jifty->log->debug( "Received " . $self->apache->method . " request for " . Jifty->web->request->path );
+        Jifty->log->info( $self->apache->method . " request for " . Jifty->web->request->path  );
         Jifty->web->setup_session;
 
         Jifty::I18N->get_language_handle;
 
         # Return from the continuation if need be
         Jifty->web->request->return_from_continuation;
-        Jifty->web->session->set_cookie;
         $self->dispatcher->handle_request();
         $self->cleanup_request();
     }

Modified: jifty/branches/virtual-models/lib/Jifty/JSON.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/JSON.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/JSON.pm	Sat Dec  1 17:17:34 2007
@@ -7,6 +7,14 @@
 
 Jifty::JSON -- Wrapper around L<JSON>
 
+=head1 SYNOPSIS
+
+  use Jifty::JSON;
+
+  # Even though you might be using JSON::Syck, use the original JSON names
+  my $obj  = jsonToObj(q! { 'x': 1, 'y': 2, 'z': 3 } !);
+  my $json = objToJson($obj);
+
 =head1 DESCRIPTION
 
 Provides a wrapper around the L<JSON> library.
@@ -26,14 +34,21 @@
 =cut
 
 BEGIN {
+    # Errors that happen here, stay here.
     local $@;
+
+    # We're hacking, so tell the nannies to leave for a minute
     no strict 'refs';
     no warnings 'once';
+
+    # If a good version of JSON::Syck is available use that...
     if (eval { require JSON::Syck; JSON::Syck->VERSION(0.05) }) {
         *jsonToObj = *_jsonToObj_syck;
         *objToJson = *_objToJson_syck;
         $JSON::Syck::ImplicitUnicode = 1;
     }
+
+    # Bummer, fallback to the pure Perl implementation
     else {
         require JSON;
         *jsonToObj = *_jsonToObj_pp;
@@ -113,4 +128,11 @@
     return JSON::objToJson($obj, $args);
 }
 
+=head1 LICENSE
+
+Jifty is Copyright 2005-2006 Best Practical Solutions, LLC.
+Jifty is distributed under the same terms as Perl itself.
+
+=cut
+
 1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,82 @@
+use strict;
+use warnings;
+
+package Jifty::Plugin::Authentication::CAS;
+use base qw/Jifty::Plugin/;
+use Authen::CAS::Client;
+
+=head1 NAME
+
+Jifty::Plugin::Authentication::CAS
+
+=head1 DESCRIPTION
+
+This may be combined with the L<Jifty::Plugin::User> plugin to provide user authentication using JA-SIG CAS protocol to your application.
+
+https is managed with Crypt::SSLeay
+
+=head1 CONFIG
+
+ in etc/config.yml
+
+  Plugins: 
+    - Authentication::CAS: 
+       CASUrl: https://auth.univ-metz.fr/cas
+       CASDomain: univ-metz.fr                  # optional: create email if login at domain is valid
+
+
+=head1 METHODS
+
+=head2 prereq_plugins
+
+This plugin depends on the L<User|Jifty::Plugin::User> plugin.
+
+=cut
+
+
+sub prereq_plugins {
+    return ('User');
+}
+
+
+my ($CAS,$domain);
+
+=head2 init
+
+load config 
+
+=cut
+
+sub init {
+    my $self = shift;
+    my %args = @_;
+
+    $CAS = Authen::CAS::Client->new ( $args{'CASUrl'} );
+    $domain = $args{'CASDomain'} || "" ;
+};
+
+
+sub CAS {
+    return $CAS;
+};
+
+sub domain {
+    return $domain;
+};
+
+=head1 TODO
+
+add a ldap config to get more attributes
+
+=head1 SEE ALSO
+
+L<Jifty::Manual::AccessControl>, L<Jifty::Plugin::User>, L<Authen::CAS::Client>
+
+=head1 LICENSE
+
+Jifty is Copyright 2005-2007 Best Practical Solutions, LLC.
+Jifty is distributed under the same terms as Perl itself.
+
+=cut
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Action/CASLogin.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Action/CASLogin.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,148 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Jifty::Plugin::Authentication::CAS::Action::CASLogin
+
+=cut
+
+package Jifty::Plugin::Authentication::CAS::Action::CASLogin;
+use base qw/Jifty::Action/;
+
+
+=head2 arguments
+
+Return the ticket form field
+
+=cut
+
+sub arguments {
+    return (
+        {
+            ticket => {
+                label          => 'cas ticket',
+                ajax_validates => 1,
+            },
+
+        }
+    );
+
+}
+
+=head2 validate_ticket ST
+
+for ajax_validates
+Makes sure that the ticket submitted is legal.
+
+
+=cut
+
+sub validate_ticket {
+    my $self  = shift;
+    my $ticket = shift;
+
+    if ( $ticket && $ticket !~ /^[A-Za-z0-9-]+$/ ) {
+        return $self->validation_error(
+            ticket => _("That doesn't look like a valid ticket.") );
+    }
+
+
+    return $self->validation_ok('ticket');
+}
+
+
+=head2 take_action
+
+Actually check the user's password. If it's right, log them in.
+Otherwise, throw an error.
+
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    my $ticket = $self->argument_value('ticket');
+
+    my ($plugin)  = Jifty->find_plugin('Jifty::Plugin::Authentication::CAS');
+
+#    my $service_url = ($ENV{SERVER_PORT} == 443)?'https://':'http://'.
+#    	$ENV{HTTP_HOST}.'/caslogin';
+    
+    my $service_url = Jifty->web->url.'/caslogin';
+
+    if (! $ticket) {
+        my $login_url = $plugin->CAS->login_url( $service_url );
+        Jifty->web->_redirect($login_url);
+        return 1;
+      }
+
+    my $r = $plugin->CAS->service_validate($service_url,$ticket);
+    my $username;
+    if ($r->is_success) {
+        $username = $r->user();
+    }
+    else {
+      Jifty->log->info("CAS error: $ticket $username");
+      return;
+    };
+     
+    my ($name,$email);
+    #TODO add a ldap conf to find name and email
+    $email = $username.'@'.$plugin->domain() if ($plugin->domain());
+
+    # Load up the user
+    my $current_user = Jifty->app_class('CurrentUser');
+    my $user = ($email) ? $current_user->new( email => $email)    # load by email to mix authentication
+                        : $current_user->new( cas_id => $username );  # else load by cas_id
+
+    # Autocreate the user if necessary
+    if ( not $user->id ) {
+        my $action = Jifty->web->new_action(
+            class           => 'CreateUser',
+            current_user    => $current_user->superuser,
+            arguments       => {
+                cas_id => $username
+            }
+        );
+        $action->run;
+
+        if ( not $action->result->success ) {
+            # Should this be less "friendly"?
+            $self->result->error(_("Sorry, something weird happened (we couldn't create a user for you).  Try again later."));
+            return;
+        }
+
+        $user = $current_user->new( cas_id => $username );
+    }
+
+    my $u = $user->user_object;
+
+
+    # Update, just in case
+    $u->__set( column => 'cas_id', value => $username ) if (!$u->cas_id);
+    $u->__set( column => 'name', value => $username ) if (!$u->name);
+    $u->__set( column => 'name', value => $name ) if ($name);
+    $u->__set( column => 'email', value => $email ) if ($email);
+ 
+    # Actually do the signin thing.
+    Jifty->web->current_user( $user );
+    Jifty->web->session->set_cookie;
+
+    # Success!
+    $self->report_success;
+
+    return 1;
+};
+
+=head2 report_success
+
+=cut
+
+sub report_success {
+    my $self = shift;
+    $self->result->message(_("Hi %1!", Jifty->web->current_user->user_object->name ));
+};
+
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Action/CASLogout.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Action/CASLogout.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,35 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Jifty::Plugin::Authentication::CAS::Action::CASLogout
+
+=cut
+
+package Jifty::Plugin::Authentication::CAS::Action::CASLogout;
+use base qw/Jifty::Action/;
+
+=head2 arguments
+
+Return the email and password form fields
+
+=cut
+
+sub arguments {
+    return ( {} );
+}
+
+=head2 take_action
+
+Nuke the current user object
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    Jifty->web->current_user(undef);
+    return 1;
+}
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Dispatcher.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Dispatcher.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,56 @@
+use strict;
+use warnings;
+
+package Jifty::Plugin::Authentication::CAS::Dispatcher;
+use Jifty::Dispatcher -base;
+
+# Put any plugin-specific dispatcher rules here.
+
+before '/caslogin' => run {
+ if (get('ticket')) {
+    # verify ticket 
+    set 'action' =>
+        Jifty->web->new_action(
+        class => 'CASLogin',
+        moniker => 'casloginbox',
+        arguments => { ticket => get('ticket') },
+        );
+
+
+  };
+
+  set 'next' => Jifty->web->request->continuation
+      || Jifty::Continuation->new(
+      request => Jifty::Request->new( path => "/" ) );
+
+};
+
+
+on '/caslogin' => run {
+
+   Jifty->web->new_action(
+       moniker => 'casloginbox',
+       class   => 'CASLogin',
+       arguments => { ticket => get('ticket') }
+       )->run;
+
+    if(Jifty->web->request->continuation) {
+        Jifty->web->request->continuation->call;
+     } else {
+           redirect '/';
+     }
+};
+
+# Log out
+before '/caslogout' => run {
+    Jifty->web->request->add_action(
+        class   => 'CASLogout',
+        moniker => 'caslogout',
+    );
+};
+
+on '/caslogout' => run {
+   redirect '/';
+};
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Mixin/Model/User.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/CAS/Mixin/Model/User.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,35 @@
+package Jifty::Plugin::Authentication::CAS::Mixin::Model::User;
+use strict;
+use warnings;
+use Jifty::DBI::Schema;
+use base 'Jifty::DBI::Record::Plugin';
+
+=head1 NAME
+
+Jifty::Plugin::Authentication::CAS::Mixin::Model::User;
+
+=head1 DESCRIPTION
+
+L<Jifty::Plugin::Authentication::CAS> mixin for the User model.  Provides an 'cas_id' column.
+
+=cut
+
+our @EXPORT = qw(has_alternative_auth);
+
+use Jifty::Plugin::Authentication::CAS::Record schema {
+
+column cas_id =>
+  type is 'text',
+  label is 'CAS ID',
+  is distinct,
+  is immutable;
+
+};
+
+=head2 has_alternative_auth
+
+=cut
+
+sub has_alternative_auth { 1 }
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,117 @@
+use strict;
+use warnings;
+
+package Jifty::Plugin::Authentication::Ldap;
+use base qw/Jifty::Plugin/;
+
+=head1 NAME
+
+Jifty::Plugin::Authentication::Ldap - ldap authentication plugin
+
+=head1 DESCRIPTION
+
+B<CAUTION:> This plugin is experimental.
+
+This may be combined with the L<Jifty::Plugin::User> plugin to provide user accounts and ldap password authentication to your application.
+
+in etc/config.yml
+
+  Plugins: 
+    - Login: {}
+    - Authentication::Ldap: 
+       LDAPhost: ldap.univ.fr           # ldap server
+       LDAPbase: ou=people,dc=.....     # base ldap
+       LDAPName: displayname            # name to be displayed (cn givenname)
+       LDAPMail: mailLocalAddress       # email used optionnal
+       LDAPuid: uid                     # optional
+
+
+
+=head2 METHODS
+
+=head2 prereq_plugins
+
+This plugin depends on the L<User|Jifty::Plugin::User> plugin.
+
+=cut
+
+
+sub prereq_plugins {
+    return ('User');
+}
+
+use Net::LDAP;
+
+
+my ($LDAP, %params);
+
+=head2 init
+
+read etc/config.yml
+
+=cut
+
+sub init {
+    my $self = shift;
+    my %args = @_;
+
+    $params{'Hostname'} = $args{LDAPhost};
+    $params{'base'} = $args{LDAPbase};
+    $params{'uid'} = $args{LDAPuid} || "uid";
+    $params{'email'} = $args{LDAPMail} || "";
+    $params{'name'} = $args{LDAPName} || "cn";
+    $LDAP = Net::LDAP->new($params{Hostname},async=>1,onerror => 'undef', debug => 0);
+}
+
+sub LDAP {
+    return $LDAP;
+}
+
+sub base {
+    return $params{'base'};
+}
+
+sub uid {
+    return $params{'uid'};
+}
+
+sub email {
+    return $params{'email'};
+};
+
+sub name {
+    return $params{'name'};
+};
+
+
+
+sub get_infos {
+    my ($self,$user) = @_;
+
+    my $result = $self->LDAP()->search (
+            base   => $self->base(),
+            filter => '(uid= '.$user.')',
+            attrs  =>  [$self->name(),$self->email()],
+            sizelimit => 1
+             );
+    my ($ret) = $result->entries;
+    my $name = $ret->get_value($self->name());
+    my $email = $ret->get_value($self->email());
+
+    return ({ name => $name, email => $email });
+};
+
+
+
+=head1 SEE ALSO
+
+L<Jifty::Manual::AccessControl>, L<Jifty::Plugin::User>, L<Net::LDAP>
+
+=head1 LICENSE
+
+Jifty is Copyright 2005-2007 Best Practical Solutions, LLC.
+Jifty is distributed under the same terms as Perl itself.
+
+=cut
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogin.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogin.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,135 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Jifty::Plugin::Authentication::Ldap::Action::LDAPLogin;
+
+=cut
+
+package Jifty::Plugin::Authentication::Ldap::Action::LDAPLogin;
+use base qw/Jifty::Action/;
+
+
+=head1 ARGUMENTS
+
+Return the login form field
+
+=cut
+
+use Jifty::Param::Schema;
+use Jifty::Action schema {
+    param ldap_id => 
+        label is _('Login'),
+        is mandatory;
+#        is ajax_validates;
+    param password =>
+        type is 'password',
+        label is _('Password'),
+        is mandatory;
+};
+
+=head2 validate_name NAME
+
+For ajax_validates.
+Makes sure that the name submitted is a legal login.
+
+
+=cut
+
+sub validate_ldap_id {
+    my $self  = shift;
+    my $name = shift;
+
+    unless ( $name =~ /^[A-Za-z0-9-]+$/ ) {
+        return $self->validation_error(
+            name => _("That doesn't look like a valid login.") );
+    }
+
+
+    return $self->validation_ok('name');
+}
+
+
+=head2 take_action
+
+Bind on ldap to check the user's password. If it's right, log them in.
+Otherwise, throw an error.
+
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    my $username = $self->argument_value('ldap_id');
+    my ($plugin)  = Jifty->find_plugin('Jifty::Plugin::Authentication::Ldap');
+    my $dn = $plugin->uid().'='.$username.','.
+        $plugin->base();
+
+
+    # Bind on ldap
+    my $msg = $plugin->LDAP()->bind($dn ,'password' =>$self->argument_value('password'));
+
+
+    unless (not $msg->code) {
+        $self->result->error(
+     _('You may have mistyped your login or password. Give it another shot?')
+        );
+        return;
+    }
+
+    # Load up the user
+    my $current_user = Jifty->app_class('CurrentUser');
+    my $user = $current_user->new( ldap_id => $username );
+
+    # Autocreate the user if necessary
+    if ( not $user->id ) {
+        my $action = Jifty->web->new_action(
+            class           => 'CreateUser',
+            current_user    => $current_user->superuser,
+            arguments       => {
+                ldap_id => $username
+            }
+        );
+        $action->run;
+
+        if ( not $action->result->success ) {
+            # Should this be less "friendly"?
+            $self->result->error(_("Sorry, something weird happened (we couldn't create a user for you).  Try again later."));
+            return;
+        }
+
+        $user = $current_user->new( ldap_id => $username );
+    }
+
+    my $infos =  $plugin->get_infos($username);
+    my $name = $infos->{name};
+    my $email = $infos->{email};
+    my $u = $user->user_object;
+
+    # Update, just in case
+    $u->__set( column => 'name', value => $name );
+    $u->__set( column => 'email', value => $email );
+
+
+    # Login!
+    Jifty->web->current_user( $user );
+    Jifty->web->session->set_cookie;
+
+    # Success!
+    $self->report_success;
+
+    return 1;
+};
+
+=head2 report_success
+
+=cut
+
+sub report_success {
+    my $self = shift;
+    $self->result->message(_("Hi %1!", Jifty->web->current_user->user_object->name ));
+};
+
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogout.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Action/LDAPLogout.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,35 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Jifty::Plugin::Authentication::Ldap::Action::LDAPLogout;
+
+=cut
+
+package Jifty::Plugin::Authentication::Ldap::Action::LDAPLogout;
+use base qw/Jifty::Action/;
+
+=head2 arguments
+
+Return the email and password form fields
+
+=cut
+
+sub arguments {
+    return ( {} );
+}
+
+=head2 take_action
+
+Nuke the current user object
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    Jifty->web->current_user(undef);
+    return 1;
+}
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Dispatcher.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Dispatcher.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,32 @@
+use strict;
+use warnings;
+
+package Jifty::Plugin::Authentication::Ldap::Dispatcher;
+use Jifty::Dispatcher -base;
+
+# Put any plugin-specific dispatcher rules here.
+
+# Log out
+before 'ldaplogout' => run {
+    Jifty->web->request->add_action(
+        class   => 'LDAPLogout',
+        moniker => 'ldaplogout',
+    );
+};
+
+
+# Login
+on 'ldaplogin' => run {
+    set 'action' =>
+        Jifty->web->new_action(
+        class => 'LDAPLogin',
+        moniker => 'ldaploginbox'
+    );
+    set 'next' => Jifty->web->request->continuation
+        || Jifty::Continuation->new(
+        request => Jifty::Request->new( path => "/" ) );
+};
+
+
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Mixin/Model/User.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/Mixin/Model/User.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,36 @@
+package Jifty::Plugin::Authentication::Ldap::Mixin::Model::User;
+use strict;
+use warnings;
+use Jifty::DBI::Schema;
+use base 'Jifty::DBI::Record::Plugin';
+use URI;
+
+=head1 NAME
+
+Jifty::Plugin::Authentication::Ldap::Mixin::Model::User;
+
+=head1 DESCRIPTION
+
+L<Jifty::Plugin::Authentication::Ldap> mixin for the User model.  Provides an 'ldap_id' column.
+
+=cut
+
+our @EXPORT = qw(has_alternative_auth);
+
+use Jifty::Plugin::Authentication::Ldap::Record schema {
+
+column ldap_id =>
+  type is 'text',
+  label is 'Ldap ID',
+  is distinct,
+  is immutable;
+
+};
+
+=head2 has_alternative_auth
+
+=cut
+
+sub has_alternative_auth { 1 }
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/View.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Ldap/View.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,50 @@
+use utf8;
+use warnings;
+use strict;
+
+=head1 NAME Jifty::Plugin::Authentication::Ldap::View
+
+This provides the templates for the pages and forms used by the ldap authentication plugin.
+
+=cut
+
+package Jifty::Plugin::Authentication::Ldap::View;
+use Jifty::View::Declare -base;
+
+{ no warnings 'redefine';
+sub page (&;$) {
+    no strict 'refs';
+    BEGIN {Jifty::Util->require(Jifty->app_class('View'))};
+    Jifty->app_class('View')->can('page')->(@_);
+}
+}
+
+template ldaplogin => page { title => _('Login!') } content {
+    show('/ldaplogin_widget');
+};
+
+
+template ldaplogin_widget => sub {
+#    title is _("Login with your Ldap account") 
+
+    my ( $action, $next ) = get( 'action', 'next' );
+    $action ||= new_action( class => 'LDAPLogin' );
+    $next ||= Jifty::Continuation->new(
+        request => Jifty::Request->new( path => "/" ) );
+    unless ( Jifty->web->current_user->id ) {
+        h3  { _('Login with your ldap account') };
+        div {
+            attr { id => 'jifty-login' };
+            Jifty->web->form->start( call => $next );
+            render_param( $action, 'ldap_id', focus => 1 );
+            render_param( $action, 'password' );
+            form_return( label => _(q{Login}), submit => $action );
+            Jifty->web->form->end();
+        };
+    } else {
+        outs( _("You're already logged in.") );
+    }
+};
+
+
+1;

Modified: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Password/Action/Login.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Password/Action/Login.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Password/Action/Login.pm	Sat Dec  1 17:17:34 2007
@@ -154,7 +154,7 @@
         }
     }
     unless ($user->email_confirmed) {
-                $self->result->error( q{You haven't confirmed your account yet.} );        return;
+                $self->result->error( _(q{You haven't confirmed your account yet.}) );        return;
                     }
 
     # Set up our login message

Modified: jifty/branches/virtual-models/lib/Jifty/Plugin/Chart/Renderer/XMLSWF.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Plugin/Chart/Renderer/XMLSWF.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Chart/Renderer/XMLSWF.pm	Sat Dec  1 17:17:34 2007
@@ -126,7 +126,7 @@
     $html .= "<embed";
     $html .= qq[ $_="@{[$tags->{embed}{$_}]}"]
         for keys %{ $tags->{embed} };
-    $html .= "></embed>\n";
+    $html .= " />\n";
     $html .= "</object>\n";
     $html .= "</div>\n";
 

Modified: jifty/branches/virtual-models/lib/Jifty/Plugin/CompressedCSSandJS.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Plugin/CompressedCSSandJS.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/CompressedCSSandJS.pm	Sat Dec  1 17:17:34 2007
@@ -145,20 +145,17 @@
 
 =cut
 
-            
 sub generate_css {
     my $self = shift;
             
     if (not defined $self->cached_css_digest or Jifty->config->framework('DevelMode')) {
         Jifty->log->debug("Generating CSS...");
-        
-        my @roots = map { Jifty::Util->absolute_path( File::Spec->catdir( $_, 'css' ) ) }
-                        Jifty->handler->view('Jifty::View::Static::Handler')->roots;
+
+        my @roots = map { Jifty::Util->absolute_path( $_ ) }
+            Jifty->handler->view('Jifty::View::Static::Handler')->roots;
     
-        CSS::Squish->roots( @roots );
-        
-        my $css = CSS::Squish->concatenate(
-            map { CSS::Squish->_resolve_file( $_, @roots ) }
+        my $css = CSS::Squish->new( roots => \@roots )->concatenate(
+            map { File::Spec->catfile('css', $_ ) }
                 @{ Jifty->web->css_files }
         );
 

Modified: jifty/branches/virtual-models/lib/Jifty/Plugin/REST/Dispatcher.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Plugin/REST/Dispatcher.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/REST/Dispatcher.pm	Sat Dec  1 17:17:34 2007
@@ -98,11 +98,24 @@
 sub stringify {
     # XXX: allow configuration to specify model fields that are to be
     # expanded
-    no warnings 'uninitialized';
-    my @r = map { ref($_) && UNIVERSAL::isa($_, 'Jifty::Record')
-                             ? reference_to_data($_) :
-                  defined $_ ? '' . $_               : undef } @_;
-    return wantarray ? @r : pop @r;
+    my @r;
+
+    for (@_) {
+        if (UNIVERSAL::isa($_, 'Jifty::Record')) {
+            push @r, reference_to_data($_);
+        }
+        elsif (UNIVERSAL::isa($_, 'Jifty::DateTime')) {
+            push @r, _datetime_to_data($_);
+        }
+        elsif (defined $_) {
+            push @r, '' . $_; # force stringification
+        }
+        else {
+            push @r, undef;
+        }
+    }
+
+    return wantarray ? @r : $r[-1];
 }
 
 =head2 reference_to_data
@@ -125,6 +138,7 @@
 
   Jifty::DBI::Collection
   Jifty::DBI::Record
+  Jifty::DateTime
 
 =cut
 
@@ -134,6 +148,7 @@
     my %types = (
         'Jifty::DBI::Collection' => \&_collection_to_data,
         'Jifty::DBI::Record'     => \&_record_to_data,
+        'Jifty::DateTime'        => \&_datetime_to_data,
     );
 
     for my $type ( keys %types ) {
@@ -168,6 +183,18 @@
     return \%data;
 }
 
+sub _datetime_to_data {
+    my $dt = shift;
+
+    # if it looks like just a date, then return just the date portion
+    return $dt->ymd
+        if lc($dt->time_zone->name) eq 'floating'
+        && $dt->hms('') eq '000000';
+
+    # otherwise let stringification take care of it
+    return $dt;
+}
+
 =head2 recurse_object_to_data REF
 
 Takes a reference, and calls C<object_to_data> on it if that is
@@ -709,13 +736,6 @@
 
     Jifty->api->is_allowed( $action_name ) or abort(403);
 
-    my $params = $action->arguments;
-    for my $key ( keys %$params ) {
-        next if not exists $params->{ $key }{'default_value'};
-        next if $action->has_argument( $key );
-        $action->argument_value( $key => $params->{ $key }{'default_value'} );
-    }
-
     $action->validate;
 
     local $@;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Recorder.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Recorder.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,132 @@
+package Jifty::Plugin::Recorder;
+use strict;
+use warnings;
+use base qw/Jifty::Plugin Class::Data::Inheritable/;
+__PACKAGE__->mk_accessors(qw/start path loghandle/);
+
+use Time::HiRes 'time';
+use YAML;
+use Jifty::Util;
+
+our $VERSION = 0.01;
+
+=head2 init
+
+init installs the trigger needed before each HTTP request. It also establishes
+the baseline for all times and creates the log path.
+
+=cut
+
+sub init {
+    my $self = shift;
+    my %args = (
+        path => 'log/requests',
+        @_,
+    );
+
+    return if $self->_pre_init;
+
+    $self->start(time);
+    $self->path($args{path});
+    Jifty::Util->make_path($args{path});
+
+    $self->loghandle($self->get_loghandle);
+
+    # if creating the loghandle failed, then we may as well not bother :)
+    if ($self->loghandle) {
+        Jifty::Handler->add_trigger(
+            before_request => sub { $self->before_request(@_) }
+        );
+    }
+}
+
+=head2 before_request
+
+Log as much of the request state as we can.
+
+=cut
+
+sub before_request
+{
+    my $self    = shift;
+    my $handler = shift;
+    my $cgi     = shift;
+
+    my $delta = time - $self->start;
+    my $request = { cgi => $cgi, ENV => \%ENV, time => $delta };
+    my $yaml = YAML::Dump($request);
+
+    eval { print { $self->loghandle } $yaml };
+    Jifty->log->error("Unable to append to request log: $@") if $@;
+}
+
+=head2 get_loghandle
+
+Creates the loghandle. The created file is named C<PATH/BOOTTIME-PID.log>.
+
+Returns C<undef> on error.
+
+=cut
+
+sub get_loghandle {
+    my $self = shift;
+
+    my $name = sprintf '%s/%d-%d.log',
+                $self->path,
+                $self->start,
+                $$;
+
+    open my $loghandle, '>', $name or do {
+        Jifty->log->error("Unable to open $name for writing: $!");
+        return;
+    };
+
+    Jifty->log->info("Logging all HTTP requests to $name.");
+
+    return $loghandle;
+}
+
+=head1 NAME
+
+Jifty::Plugin::Recorder - record HTTP requests for playback
+
+=head1 DESCRIPTION
+
+This plugin will log all HTTP requests as YAML. The logfiles can be used by
+C<jifty playback> (provided with this plugin) to replay the logged requests.
+This can be handy for perfomance tuning, debugging, and testing.
+
+=head1 USAGE
+
+Add the following to your site_config.yml
+
+ framework:
+   Plugins:
+     - Recorder: {}
+
+=head2 OPTIONS
+
+=over 4
+
+=item path
+
+The path for creating request logs. Default: log/requests. This directory will
+be created for you, if necessary.
+
+=back
+
+=head1 SEE ALSO
+
+L<Jifty::Plugin::Recorder::Command::Playback>, L<HTTP::Server::Simple::Recorder>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2007 Best Practical Solutions
+
+This is free software and may be modified and distributed under the same terms as Perl itself.
+
+=cut
+
+1;
+
+

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/Recorder/Command/Playback.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Recorder/Command/Playback.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,199 @@
+#!/usr/bin/env perl
+package Jifty::Plugin::Recorder::Command::Playback;
+
+# note that you'll need a patch to App::CLI to allow arbitrary
+# files to define new commands
+
+package Jifty::Script::Playback;
+use strict;
+use warnings;
+
+use base qw/App::CLI::Command/;
+use Time::HiRes 'sleep';
+
+our $start = time; # for naming log files
+our $path = 'log/playback';
+
+=head1 Jifty::Script::Playback - Play back request logs
+
+=head1 DESCRIPTION
+
+L<Jifty::Plugin::Recorder> lets you record a request log. Using this command
+you can play back request logs. This can be handy for performance tuning,
+debugging, and testing.
+
+=head1 API
+
+=head2 options
+
+This command takes three options. Any arguments that are not options will be
+interpreted as files to play back. Files will be played back in the order they
+are given on the command line.
+
+=over 4
+
+=item max
+
+The maximum time to wait between requests. By default there is no maximum and
+requests will be made exactly as they are in the log.
+
+=item quiet
+
+Suppress TRACE, DEBUG, and INFO log levels.
+
+=item dbiprof
+
+Enable DBI profiling.
+
+=back
+
+=cut
+
+sub options {
+    (
+     'max=s'    => 'max',
+     'quiet'    => 'quiet',
+     'dbiprof'  => 'dbiprof',
+    )
+}
+
+=head2 run
+
+Run takes no arguments. It goes through most of the motions of starting a
+server, except it won't let the server accept incoming requests. It will then
+start playing back the request logs. Once finished, it will exit normally.
+
+=cut
+
+sub run {
+    my $self = shift;
+    Jifty->new();
+
+    # Purge stale mason cache data
+    my $data_dir = Jifty->config->framework('Web')->{'DataDir'};
+    my $server_class = Jifty->config->framework('Web')->{'ServerClass'} || 'Jifty::Server';
+    Jifty::Util->require($server_class);
+
+    if (-d $data_dir) {
+        File::Path::rmtree(["$data_dir/cache", "$data_dir/obj"]);
+    }
+    else {
+        File::Path::mkpath([$data_dir]);
+    }
+
+    Jifty->handle->dbh->{Profile} = '6/DBI::ProfileDumper'
+        if $self->{dbiprof};
+
+    Log::Log4perl->get_logger($server_class)->less_logging(3)
+        if $self->{quiet};
+
+    $server_class->new();
+    # we're not calling $server_class->run because we don't want any other
+    # requests to come in during the playback. but we do want the server
+    # to be around.
+
+    # now read in the YAML and do our dark deeds
+    my $file_num = 0;
+    Jifty::Util->make_path($path.'/'.$start);
+    for my $file (@ARGV) {
+        my @requests = YAML::LoadFile($file);
+        $self->play_requests(++$file_num, @requests);
+    }
+}
+
+=head2 play_request REQUEST
+
+Plays back a single request, right now, through Jifty->handler. It expects
+C<< $request->{ENV} >> to be a hashref which will set C<%ENV>. It expects
+C<< $request->{cgi} >> to be a CGI object which will be passed to
+C<< Jifty->handler->handle_request >>.
+
+=cut
+
+sub play_request {
+    my $self     = shift;
+    my $request  = shift;
+    my $filename = shift;
+
+    %ENV = %{ $request->{ENV} };
+
+    # this doesn't use "select $newhandle" because a few places in Jifty use
+    # print STDOUT
+    local *STDOUT;
+
+    Jifty->log->info("Logging request's output to $filename.");
+
+    open *STDOUT, '>', $filename
+        or die "Unable to open $filename for writing: $!";
+
+    Jifty->handler->handle_request(cgi => $request->{cgi});
+
+    close *STDOUT;
+}
+
+=head2 play_requests NUMBER, REQUESTs
+
+Plays through a list of requests, sleeping between each. Each request should be
+a hashref with fields C<time> (a possibly fractional number of seconds,
+representing the time of the request, relative to when the server started);
+C<ENV> (used to set C<%ENV>); and C<cgi> (passed to
+Jifty->handler->handle_request).
+
+The NUMBER is used in logfile naming so different sets of requests don't
+overwrite the same file.
+
+=cut
+
+sub play_requests {
+    my $self    = shift;
+    my $set_num = shift;
+
+    my $current_time = 0;
+    my $req_num = 0;
+
+    for my $request (@_) {
+        ++$req_num;
+
+        $request->{time} -= $current_time;
+        $request->{time} = $self->{max}
+            if defined($self->{max}) && $request->{time} > $self->{max};
+
+        Jifty->log->info("Next request in $request->{time} seconds.");
+        sleep $request->{time};
+        $current_time += $request->{time};
+
+        my $filename = sprintf '%s/%d/%d-%d',
+                        $path,
+                        $start,
+                        $set_num,
+                        $req_num;
+
+        $self->play_request($request, $filename);
+    }
+}
+
+=head2 filename
+
+This is used as a hack to get L<App::CLI> to retrieve our POD correctly.
+
+Inner packages are not given in C<%INC>. If anyone finds a way around this,
+please let us know.
+
+=cut
+
+sub filename { __FILE__ }
+
+=head1 SEE ALSO
+
+L<Jifty::Plugin::Recorder>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2007 Best Practical Solutions
+
+This is free software and may be modified and distributed under the same terms as Perl itself.
+
+=cut
+
+1;
+

Modified: jifty/branches/virtual-models/lib/Jifty/Record.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Record.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Record.pm	Sat Dec  1 17:17:34 2007
@@ -558,6 +558,23 @@
     $value;
 }
 
+=head2 as_user CurrentUser
+
+Returns a copy of this object with the current_user set to the given
+current_user. This is a way to act on behalf of a particular user (perhaps the
+owner of the object)
+
+=cut
+
+sub as_user {
+    my $self = shift;
+    my $user = shift;
+
+    my $clone = $self->new( current_user => $user );
+    $clone->load( $self->id );
+    return $clone;
+}
+
 =head2 as_superuser
 
 Returns a copy of this object with the current_user set to the
@@ -568,10 +585,7 @@
 
 sub as_superuser {
     my $self = shift;
-
-    my $clone = $self->new( current_user => $self->current_user->superuser );
-    $clone->load( $self->id );
-    return $clone;
+    return $self->as_user( $self->current_user->superuser );
 }
 
 =head2 delete PARAMHASH
@@ -613,6 +627,15 @@
 
 sub _brief_description {'name'}
 
+=head2 null_reference
+
+By default, L<Jifty::DBI::Record> returns C<undef> on non-existant
+related fields; Jifty prefers to get back an object with an undef id.
+
+=cut
+
+sub null_reference { 0 }
+
 =head2 _new_collection_args
 
 Overrides the default arguments which this collection passes to new

Modified: jifty/branches/virtual-models/lib/Jifty/Request.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Request.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Request.pm	Sat Dec  1 17:17:34 2007
@@ -122,9 +122,9 @@
 
     # Check it for something appropriate
     if ($data) {
-        if ($ct eq "text/x-json") {
+        if ($ct =~ m{^text/x-json}) {
             return $self->from_data_structure(eval{Jifty::JSON::jsonToObj($data)}, $cgi);
-        } elsif ($ct eq "text/x-yaml") {
+        } elsif ($ct =~ m{^text/x-yaml}) {
             return $self->from_data_structure(eval{Jifty::YAML::Load($data)}, $cgi);
         }
     }

Modified: jifty/branches/virtual-models/lib/Jifty/RightsFrom.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/RightsFrom.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/RightsFrom.pm	Sat Dec  1 17:17:34 2007
@@ -75,10 +75,10 @@
 
 sub export_curried_sub {
     my %args = (
-        sub_name     => undef,
-        export_to => undef,
-        as           => undef,
-        args        =>  undef,
+        sub_name   => undef,
+        export_to  => undef,
+        as         => undef,
+        args       => undef,
         @_
     );
     no strict 'refs';
@@ -109,6 +109,7 @@
     $right = 'update' if $right ne 'read';
     my $obj;
 
+    $col_name =~ s/_id$//;
     my $column   = $self->column($col_name);
     my $obj_type = $column->refers_to();
 

Modified: jifty/branches/virtual-models/lib/Jifty/Script/Schema.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Script/Schema.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Script/Schema.pm	Sat Dec  1 17:17:34 2007
@@ -48,14 +48,28 @@
     if ( $self->{create_all_tables} ) {
         $self->create_all_tables();
     } elsif ( $self->{'setup_tables'} ) {
-        $self->upgrade_jifty_tables();
-        $self->upgrade_application_tables();
-        $self->upgrade_plugin_tables();
+        $self->run_upgrades();
     } else {
         print "Done.\n";
     }
 }
 
+
+=head2 run_upgrades
+
+Take the actions we need in order to bring an existing database up to current.
+
+=cut
+
+sub run_upgrades {
+    my $self = shift;
+        $self->upgrade_jifty_tables();
+        $self->upgrade_application_tables();
+        $self->upgrade_plugin_tables();
+
+}
+
+
 =head2 setup_environment
 
 Sets up a minimal Jifty environment.
@@ -237,13 +251,15 @@
         $pluginvs{ $plugin_class } = version->new( $plugin->version );
     }
 
+    MODEL:
     for my $model ( @models) {
+        my $plugin_root = Jifty->app_class('Plugin').'::';
+
         # TODO XXX FIXME:
         #   This *will* try to generate SQL for abstract base classes you might
         #   stick in $AC::Model::.
         if ($model->can('since')) {
             my $app_class   = Jifty->app_class;
-            my $plugin_root = Jifty->app_class('Plugin');
 
             my $installed_version = 0;
 
@@ -268,10 +284,22 @@
             if ($installed_version < $model->since) {
                 # XXX Is this message correct? 
                 $log->info("Skipping $model, as it should already be in the database");
-                next;
+                next MODEL;
             }
         }
 
+        if ($model =~ /^(?:Jifty::Plugin::|$plugin_root)/
+                and $model =~ /::Model::(.*)$/) {
+            my $model_name = $1;
+
+            # Check to make sure this model is not overridden in the app,
+            # in such cases we don't want to try to create the same table
+            # twice, so let the app model do it rather than the plugin
+            my $app_model = Jifty->app_class($model_name);
+            $app_model->require;
+            next MODEL unless $app_model->can('_autogenerated');
+        }
+
         $log->info("Using $model, as it appears to be new.");
 
         $self->schema->_check_reserved($model)

Modified: jifty/branches/virtual-models/lib/Jifty/Test.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Test.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Test.pm	Sat Dec  1 17:17:34 2007
@@ -13,7 +13,8 @@
 use File::Spec;
 use File::Temp;
 use Hash::Merge;
-use Cwd 'abs_path';
+use Digest::MD5 qw/md5_hex/;
+use Cwd qw/abs_path cwd/;
 
 =head1 NAME
 
@@ -363,7 +364,8 @@
             },
             Mailer => 'Jifty::Test',
             MailerArgs => [],
-            LogLevel => 'WARN'
+            LogLevel => 'WARN',
+            TestMode => 1,
         }
     };
 
@@ -373,16 +375,13 @@
 
 
 sub _testfile_to_dbname {
-    if ($ENV{JIFTY_FAST_TEST}) {
-        return 'fasttest';
-    }
-    else {
+    return 'fasttest' if $ENV{JIFTY_FAST_TEST};
     my $dbname = lc($0);
     $dbname =~ s/\.t$//;
-    $dbname =~ s/[-_\.\/\\]//g;
-    $dbname = substr($dbname,-32,32);	
+    $dbname =~ s/(\W|[_-])+//g;
+    $dbname .= substr(md5_hex(cwd()), 0, 8);
+    $dbname = substr($dbname,-32,32);
     return $dbname;
-    } 
 }
 
 =head2 make_server

Modified: jifty/branches/virtual-models/lib/Jifty/Test/WWW/Mechanize.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Test/WWW/Mechanize.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Test/WWW/Mechanize.pm	Sat Dec  1 17:17:34 2007
@@ -413,12 +413,17 @@
 sub follow_link_ok {
     my $self = shift;
 
-    # Remove reason from end if it's there
-    pop @_ if @_ % 2;
 
     # Test::WWW::Mechanize allows passing in a hashref of arguments, so we should to
     if  ( ref($_[0]) eq 'HASH') {
+        # if the user is pashing in { text => 'foo' } ...
+
         @_ = %{$_[0]};
+    } elsif (@_ % 2 ) {
+        # IF the user is passing in text => 'foo' ,"Cicked the right thing"
+        # Remove reason from end if it's there
+        pop @_ ;
+
     }
     carp("Couldn't find link") unless $self->follow_link(@_);
     {

Modified: jifty/branches/virtual-models/lib/Jifty/Test/WWW/Selenium.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Test/WWW/Selenium.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Test/WWW/Selenium.pm	Sat Dec  1 17:17:34 2007
@@ -114,7 +114,7 @@
 	    close *STDOUT;
 	}
 	$ENV{LANG} = $args{lang} || 'en_US.UTF-8';
-	$ENV{PATH} = "$ENV{PATH}:/usr/lib/firefox";
+	$ENV{PATH} = "$ENV{PATH}:/usr/lib/firefox:/usr/lib/mozilla-firefox";
 	Test::More::diag "start selenium rc [$$]";
 	local $SIG{CHLD} = \&_REAPER;
 	local $SIG{TERM} = sub { exit 0 };

Modified: jifty/branches/virtual-models/lib/Jifty/Util.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Util.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Util.pm	Sat Dec  1 17:17:34 2007
@@ -226,7 +226,13 @@
     my $whole_path = shift;
     return 1 if (-d $whole_path);
     Jifty::Util->require('File::Path');
-    File::Path::mkpath([$whole_path]);
+
+    local $@;
+    eval { File::Path::mkpath([$whole_path]) };
+
+    if ($@) {
+        Jifty->log->fatal("Unable to make path: $whole_path: $@")
+    }
 }
 
 =head2 require PATH
@@ -256,16 +262,21 @@
 
     return 1 if $self->already_required($class);
 
-    my $pkg = $class .".pm";
-    $pkg =~ s/::/\//g;
-    my $retval = eval  {CORE::require "$pkg"} ;
+    # .pm might already be there in a weird interaction in Module::Pluggable
+    my $file = $class;
+    $file .= ".pm"
+        unless $file =~ /\.pm$/;
+
+    $file =~ s/::/\//g;
+
+    my $retval = eval  {CORE::require "$file"} ;
     my $error = $@;
     if (my $message = $error) { 
         $message =~ s/ at .*?\n$//;
-        if ($args{'quiet'} and $message =~ /^Can't locate $pkg/) {
+        if ($args{'quiet'} and $message =~ /^Can't locate $file/) {
             return 0;
         }
-        elsif ( $error !~ /^Can't locate $pkg/) {
+        elsif ( $error !~ /^Can't locate $file/) {
             die $error;
         } else {
             Jifty->log->error(sprintf("$message at %s line %d\n", (caller(1))[1,2]));

Modified: jifty/branches/virtual-models/lib/Jifty/View.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/View.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/View.pm	Sat Dec  1 17:17:34 2007
@@ -32,6 +32,7 @@
 =cut
 
 sub out_method {
+    Jifty->web->session->set_cookie;
     my $r = Jifty->handler->apache;
 
     $r->content_type || $r->content_type('text/html; charset=utf-8'); # Set up a default

Modified: jifty/branches/virtual-models/lib/Jifty/View/Declare.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/View/Declare.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/View/Declare.pm	Sat Dec  1 17:17:34 2007
@@ -23,7 +23,7 @@
 
 L<Template::Declare> is a templating system using a declarative syntax built on top of Perl. This provides a templating language built in a similar style to the dispatcher language in L<Jifty::Dispatcher>, the model language in L<Jifty::DBI::Schema>, and the action language in L<Jifty::Param::Schema>.
 
-To use this view system, you must declare a class named C<MyApp::View> (where I<MyApp> is the name of your Jifyt application). Use this library class to bring in all the details needed to make it work:
+To use this view system, you must declare a class named C<MyApp::View> (where I<MyApp> is the name of your Jifty application). Use this library class to bring in all the details needed to make it work:
 
   package MyApp::View;
   use Jifty::View::Declare -base;

Modified: jifty/branches/virtual-models/lib/Jifty/View/Declare/CRUD.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/View/Declare/CRUD.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/View/Declare/CRUD.pm	Sat Dec  1 17:17:34 2007
@@ -252,9 +252,25 @@
 sub display_columns {
     my $self = shift;
     my $action = shift;
-     return   grep { !( m/_confirm/ || lc $action->arguments->{$_}{render_as} eq 'password' ) } $action->argument_names;
+     return   $action->argument_names;
+     #return   grep { !( m/_confirm/ ||  $action->arguments->{$_}{unreadable} ) } $action->argument_names;
 }
 
+
+sub edit_columns {
+    my $self = shift; 
+    return $self->display_columns(@_);
+}
+
+sub create_columns {
+    my $self = shift; 
+    return $self->edit_columns(@_);
+
+
+}
+
+
+
 =head1 TEMPLATES
 
 =head2 index.html
@@ -329,8 +345,8 @@
             }; 
         }
         show ('./view_item_controls', $record, $update); 
-        hr {};
     };
+    hr {};
 
 };
 
@@ -536,7 +552,8 @@
         render_region(
             name     => 'new_item',
             path     => $fragment_for_new_item,
-            defaults => { object_type => $object_type },
+            defaults => {
+                        object_type => $object_type },
         );
     }
 };
@@ -547,7 +564,12 @@
 
 =cut
 
-private template 'no_items_found' => sub { outs(_("No items found.")) };
+private template 'no_items_found' => sub {
+    div {
+        { class is 'no_items' };
+        outs( _("No items found.") );
+    }
+};
 
 =head2 list_items $collection $item_path
 
@@ -589,7 +611,7 @@
 private template 'paging_top' => sub {
     my $self       = shift;
     my $collection = shift;
-    my $page       = shift;
+    my $page       = shift || 1;
 
     if ( $collection->pager->last_page > 1 ) {
         span {
@@ -637,6 +659,23 @@
     };
 };
 
+
+=head2 new_item $action
+
+Renders the action $Action, handing it the array ref returned by L</display_columns>.
+
+=cut
+
+private template 'create_item' => sub {
+    my $self = shift;
+    my $action = shift;
+   foreach my $field ($self->create_columns($action)) {
+            div { { class is 'create-argument-'.$field}
+                render_param($action, $field) ;
+        }
+   }
+};
+
 =head2 edit_item $action
 
 Renders the action $Action, handing it the array ref returned by L</display_columns>.
@@ -646,7 +685,7 @@
 private template 'edit_item' => sub {
     my $self = shift;
     my $action = shift;
-   foreach my $field ($self->display_columns($action)) {
+   foreach my $field ($self->edit_columns($action)) {
             div { { class is 'update-argument-'.$field}
     render_param($action, $field) ;
         }
@@ -668,7 +707,15 @@
 
     div {
         { class is 'crud create item inline' };
-        show('./edit_item', $create);
+        show('./create_item', $create);
+        show('./new_item_controls', $create);
+    }
+};
+
+private template 'new_item_controls' => sub {
+        my $self = shift;
+        my $create = shift;
+    my ( $object_type ) = ( $self->object_type);
 
         outs(
             Jifty->web->form->submit(
@@ -676,9 +723,7 @@
                 onclick => [
                     { submit       => $create },
                     { refresh_self => 1 },
-                    {   element =>
-                            Jifty->web->current_region->parent->get_element(
-                            'div.list'),
+                    {   element => Jifty->web->current_region->parent->get_element( 'div.list'),
                         append => $self->fragment_for('view'),
                         args   => {
                             object_type => $object_type,
@@ -687,9 +732,11 @@
                     },
                 ]
             )
-        );
-    }
-};
+        )
+    };
+
+
+
 
 =head1 SEE ALSO
 

Modified: jifty/branches/virtual-models/lib/Jifty/View/Declare/Handler.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/View/Declare/Handler.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/View/Declare/Handler.pm	Sat Dec  1 17:17:34 2007
@@ -81,6 +81,7 @@
     my $r = Jifty->handler->apache;
     $r->content_type || $r->content_type('text/html; charset=utf-8'); # Set up a default
     unless ( Jifty->handler->apache->http_header_sent || Jifty->web->request->is_subrequest ) {
+        Jifty->web->session->set_cookie;
         Jifty->handler->apache->send_http_header;
     }
 

Modified: jifty/branches/virtual-models/lib/Jifty/View/Declare/Helpers.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/View/Declare/Helpers.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/View/Declare/Helpers.pm	Sat Dec  1 17:17:34 2007
@@ -295,7 +295,7 @@
         my $wrapper = Jifty->app_class('View')->can('wrapper') || \&wrapper;
         my @metadata = $meta ? $meta->() : ();
         my $metadata = $#metadata == 0 ? $metadata[0] : {@metadata};
-        local *is::title = sub { warn "Can't use 'title is' when mixing mason and TD" };
+        local *is::title = sub { Carp::carp "Can't use 'title is' when mixing mason and TD" };
         $wrapper->( sub { $code->( $self, $metadata ) }, $metadata );
     }
 }

Modified: jifty/branches/virtual-models/lib/Jifty/View/Mason/Handler.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/View/Mason/Handler.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/View/Mason/Handler.pm	Sat Dec  1 17:17:34 2007
@@ -186,6 +186,8 @@
 sub handle_comp {
     my ($self, $comp, $args) = @_;
 
+    Jifty->web->session->set_cookie;
+
     # Set up the global
     my $r = Jifty->handler->apache;
     $self->interp->set_global('$r', $r);

Modified: jifty/branches/virtual-models/lib/Jifty/View/Static/Handler.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/View/Static/Handler.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/View/Static/Handler.pm	Sat Dec  1 17:17:34 2007
@@ -243,6 +243,9 @@
         $apache->header_out( Status => 200 );
         $apache->content_type($mime_type);
         my $now = time();
+     
+        $apache->header_out('Cache-Control' =>  'max-age=259200, public');
+
         $apache->header_out(Expires =>  HTTP::Date::time2str($now + 31536000));  # Expire in a year
         $apache->header_out('Last-Modified' =>  HTTP::Date::time2str( $file_info[9]));
         $apache->header_out('Content-Length' => $file_info[7]) unless ($compression eq 'gzip');  

Modified: jifty/branches/virtual-models/lib/Jifty/Web.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Web.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Web.pm	Sat Dec  1 17:17:34 2007
@@ -784,6 +784,10 @@
     # Headers..
     $apache->header_out( Location => $page );
     $apache->header_out( Status => 302 );
+
+    # cookie has to be sent or returning from continuations breaks
+    Jifty->web->session->set_cookie;
+
     $apache->send_http_header();
 
     # Mason abort, or dispatcher abort out of here

Modified: jifty/branches/virtual-models/lib/Jifty/Web/Form/Element.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Web/Form/Element.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Web/Form/Element.pm	Sat Dec  1 17:17:34 2007
@@ -167,11 +167,11 @@
 
 =cut
 
-sub accessors { shift->handlers, qw(class title key_binding key_binding_label id label tooltip continuation) }
+sub accessors { shift->handlers, qw(class title key_binding key_binding_label id label tooltip continuation rel) }
 __PACKAGE__->mk_accessors(qw(_onclick _onchange _ondblclick _onmousedown _onmouseup _onmouseover 
                              _onmousemove _onmouseout _onfocus _onblur _onkeypress _onkeydown 
                              _onkeyup _onselect
-                             class title key_binding key_binding_label id label tooltip continuation));
+                             class title key_binding key_binding_label id label tooltip continuation rel));
 
 =head2 new PARAMHASH OVERRIDE
 
@@ -560,7 +560,7 @@
         if ( @fragments or ( !$actions || %$actions ) ) {
 
             my $update = Jifty->web->escape(
-                "update( "
+                "Jifty.update( "
                     . Jifty::JSON::objToJson(
                     {   actions      => $actions,
                         action_arguments => $action_arguments,

Modified: jifty/branches/virtual-models/lib/Jifty/Web/Form/Field.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Web/Form/Field.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Web/Form/Field.pm	Sat Dec  1 17:17:34 2007
@@ -489,7 +489,7 @@
     $field .= qq! name="@{[ $self->input_name ]}"! if ($self->input_name);
     $field .= qq! title="@{[ $self->title ]}"! if ($self->title);
     $field .= qq! id="@{[ $self->element_id ]}"!;
-    $field .= qq! value="@{[Jifty->web->escape($self->current_value)]}"! if defined $self->current_value;
+    $field .= qq! value="@{[$self->canonicalize_value(Jifty->web->escape($self->current_value))]}"! if defined $self->current_value;
     $field .= $self->_widget_class; 
     $field .= qq! size="@{[ $self->max_length() ]}" maxlength="@{[ $self->max_length() ]}"! if ($self->max_length());
     $field .= qq! autocomplete="off"! if defined $self->disable_autocomplete;
@@ -501,6 +501,17 @@
 }
 
 
+=head2 canonicalize_value
+
+Called when a value is about to be displayed. Can be overridden to, for example,
+display only the yyyy-mm-dd portion of a DateTime.
+
+=cut
+
+sub canonicalize_value {
+    my $self = shift;
+    return $_[0];
+}
 
 =head2 other_widget_properties
 
@@ -551,7 +562,7 @@
     my $field = '<span';
     $field .= qq! class="@{[ $self->classes ]}"> !;
     # XXX: force stringify the value because maketext is buggy with overloaded objects.
-    $field .= Jifty->web->escape("@{[$self->current_value]}") if defined $self->current_value;
+    $field .= $self->canonicalize_value(Jifty->web->escape("@{[$self->current_value]}")) if defined $self->current_value;
     $field .= qq!</span>\n!;
     Jifty->web->out($field);
     return '';

Modified: jifty/branches/virtual-models/lib/Jifty/Web/Form/Field/Date.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Web/Form/Field/Date.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Web/Form/Field/Date.pm	Sat Dec  1 17:17:34 2007
@@ -34,4 +34,22 @@
 }
 
 
+=head2 canonicalize_value
+
+If the value is a DateTime, return just the ymd portion of it.
+
+=cut
+
+sub canonicalize_value {
+    my $self  = shift;
+    my $value = $self->current_value;
+
+    if (UNIVERSAL::isa($value, 'DateTime')) {
+        $value = $value->ymd;
+    }
+
+    return $value;
+}
+
+
 1;

Modified: jifty/branches/virtual-models/lib/Jifty/Web/Form/Field/Select.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Web/Form/Field/Select.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Web/Form/Field/Select.pm	Sat Dec  1 17:17:34 2007
@@ -22,7 +22,7 @@
     my $field = qq! <select !;
     $field .= qq! name="@{[ $self->input_name ]}"!;
     $field .= qq! id="@{[ $self->element_id ]}"!;
-    $field .= qq! id="@{[ $self->title ]}"! if ($self->title);
+    $field .= qq! title="@{[ $self->title ]}"! if ($self->title);
     $field .= $self->_widget_class;
     $field .= $self->javascript;
     $field .= qq!      >\n!;

Modified: jifty/branches/virtual-models/lib/Jifty/Web/Form/Link.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Web/Form/Link.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Web/Form/Link.pm	Sat Dec  1 17:17:34 2007
@@ -31,8 +31,8 @@
 
 =cut
 
-sub accessors { shift->SUPER::accessors(), qw(url escape_label tooltip target); }
-__PACKAGE__->mk_accessors(qw(url escape_label tooltip target));
+sub accessors { shift->SUPER::accessors(), qw(url escape_label tooltip target rel); }
+__PACKAGE__->mk_accessors(qw(url escape_label tooltip target rel));
 
 =head2 new PARAMHASH
 
@@ -75,6 +75,7 @@
         tooltip      => undef,
         escape_label => 1,
         class        => '',
+        rel          => '',
         target       => '' }, $args );
 
     return $self;
@@ -110,6 +111,7 @@
     $output .= (qq( title="@{[$tooltip]}"))       if defined $tooltip;
     $output .= (qq( target="@{[$self->target]}")) if $self->target;
     $output .= (qq( accesskey="@{[$self->key_binding]}")) if $self->key_binding;
+    $output .= (qq( rel="@{[$self->rel]}"))       if $self->rel;
     $output .= (qq( href="@{[Jifty->web->escape($self->url)]}"));
     $output .= ( $self->javascript() );
     $output .= (qq(>$label</a>));

Modified: jifty/branches/virtual-models/share/po/fr.po
==============================================================================
--- jifty/branches/virtual-models/share/po/fr.po	(original)
+++ jifty/branches/virtual-models/share/po/fr.po	Sat Dec  1 17:17:34 2007
@@ -56,64 +56,79 @@
 "\n"
 "%2\n"
 
-#: lib/Jifty/Action/Record/Search.pm:125
+#: lib/Jifty/Plugin/Authentication/Facebook/View.pm:43
+msgid ""
+" id is 'facebook_link' };\n"
+"        span { _(\"Login to Facebook now to link it with your current account!\") };\n"
+"        a {{ href is $plugin->get_link_url };\n"
+"            img {{ src is 'http://static.ak.facebook.com/images/devsite/facebook_login.gif', border is '0' "
+msgstr ""
+
+#: lib/Jifty/Plugin/Authentication/Facebook/View.pm:26
+msgid ""
+" id is 'facebook_login' };\n"
+"        span { _(\"Login to Facebook now to get started!\") };\n"
+"        a {{ href is $plugin->get_login_url };\n"
+"            img {{ src is 'http://static.ak.facebook.com/images/devsite/facebook_login.gif', border is '0' "
+msgstr ""
+
+#: lib/Jifty/Action/Record/Search.pm:172
 msgid "!=>< allowed"
 msgstr "!=>< autorisés"
 
-#: lib/Jifty/Notification.pm:94
+#: lib/Jifty/Notification.pm:108 lib/Jifty/Notification.pm:125
 #. ($appname, Jifty->config->framework('AdminEmail')
 msgid "%1 <%2>"
 msgstr ""
 
-#: lib/Jifty/Action/Record/Search.pm:115
+#: lib/Jifty/Action/Record/Search.pm:143
 #. ($label)
 msgid "%1 after"
 msgstr "%1 après"
 
-#: lib/Jifty/Action/Record/Search.pm:116
+#: lib/Jifty/Action/Record/Search.pm:146
 #. ($label)
 msgid "%1 before"
 msgstr "%1 avant"
 
-#: lib/Jifty/Action/Record/Search.pm:112
+#: lib/Jifty/Action/Record/Search.pm:133
 #. ($label)
 msgid "%1 contains"
 msgstr "%1 contient"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:141
-#. ($collection-> count)
+#:
 msgid "%1 entries"
 msgstr "%1 enregistements"
 
-#: lib/Jifty/Action/Record/Search.pm:123
+#: lib/Jifty/Action/Record/Search.pm:166
 #. ($label)
 msgid "%1 greater or equal to"
-msgstr "%1 supérieur ou égal à "
+msgstr "%1 supérieur ou égal à"
 
-#: lib/Jifty/Action/Record/Search.pm:121
+#: lib/Jifty/Action/Record/Search.pm:160
 #. ($label)
 msgid "%1 greater than"
-msgstr "%1 supérieur à "
+msgstr "%1 supérieur à"
 
-#: lib/Jifty/Action/Record/Search.pm:108
+#: lib/Jifty/Action/Record/Search.pm:121
 #. ($label)
 msgid "%1 is not"
 msgstr "%1 n'est pas"
 
-#: lib/Jifty/Action/Record/Search.pm:113
+#: lib/Jifty/Action/Record/Search.pm:136
 #. ($label)
 msgid "%1 lacks"
 msgstr "%1 ne contient pas"
 
-#: lib/Jifty/Action/Record/Search.pm:124
+#: lib/Jifty/Action/Record/Search.pm:169
 #. ($label)
 msgid "%1 less or equal to"
 msgstr "%1 inférieur ou égal"
 
-#: lib/Jifty/Action/Record/Search.pm:122
+#: lib/Jifty/Action/Record/Search.pm:163
 #. ($label)
 msgid "%1 less than"
-msgstr "%1 inférieur à "
+msgstr "%1 inférieur à"
 
 #: share/web/templates/__jifty/error/mason_internal_error:31 share/web/templates/__jifty/error/mason_internal_error:35 share/web/templates/__jifty/error/mason_internal_error:39
 #. ($path, $line)
@@ -126,20 +141,24 @@
 msgid "%1 seconds"
 msgstr "%1 secondes"
 
-#: lib/Jifty/Action/Record/Search.pm:117
+#: lib/Jifty/Action/Record/Search.pm:149
 #. ($label)
 msgid "%1 since"
 msgstr "%1 depuis le"
 
-#: lib/Jifty/Action/Record/Search.pm:118
+#: lib/Jifty/Action/Record/Search.pm:152
 #. ($label)
 msgid "%1 until"
 msgstr "%1 depuis"
 
-#: lib/Jifty/Action/Record/Search.pm:77
+#: lib/Jifty/Action/Record/Search.pm:81
 msgid "(any)"
 msgstr ""
 
+#: lib/Jifty/Plugin/AutoReference.pm:95
+msgid "- none -"
+msgstr "- aucun -"
+
 #: lib/Jifty/Plugin/Authentication/Password/Action/ConfirmEmail.pm:58
 msgid ". Your email address has now been confirmed."
 msgstr ". Votre adresse mail est confirmée."
@@ -148,32 +167,28 @@
 msgid "A link to reset your password has been sent to your email account."
 msgstr "Une URL pour changer votre mots de passe, vous a été transmis par mail. "
 
-#: lib/Jifty/Notification.pm:96
+#: lib/Jifty/Notification.pm:110 lib/Jifty/Notification.pm:127
 #. ($appname)
 msgid "A notification from %1!"
-msgstr "Message de %1!"
-
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:443
-msgid "Actions"
-msgstr ""
+msgstr "Message de %1 !"
 
-#: lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm:28
+#: lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm:33
 msgid "Administration"
 msgstr ""
 
-#: lib/Jifty/View/Declare/Helpers.pm:363 share/web/templates/_elements/wrapper:11
+#: lib/Jifty/View/Declare/Page.pm:176 share/web/templates/_elements/wrapper:11
 msgid "Administration mode is enabled."
 msgstr "Mode d'administration activé."
 
-#: lib/Jifty/View/Declare/Helpers.pm:360 share/web/templates/_elements/wrapper:11
+#: lib/Jifty/View/Declare/Page.pm:173 share/web/templates/_elements/wrapper:11
 msgid "Alert"
 msgstr "Alerte"
 
-#: lib/Jifty/Action/Record/Create.pm:82
+#: lib/Jifty/Action/Record/Create.pm:92
 msgid "An error occurred.  Try again later"
 msgstr "Erreur. Ré-essayez plus tard"
 
-#: lib/Jifty/Action/Record/Search.pm:129
+#: lib/Jifty/Action/Record/Search.pm:177
 msgid "Any field contains"
 msgstr "Un champs contient"
 
@@ -181,15 +196,15 @@
 msgid "Anyway, the software has logged this error."
 msgstr "Cette erreur a été enregistrée par le logiciel."
 
-#: lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm:19
+#: lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm:53
 msgid "Authentication token"
 msgstr "Token d'authentification"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:489 lib/Jifty/Plugin/AdminUI/View-not-yet.pm:59
+#:
 msgid "Back to the admin console"
 msgstr "Retour à l'interface d'admin"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:461
+#:
 msgid "Back to the application"
 msgstr "Retour à l'application"
 
@@ -202,7 +217,7 @@
 msgid "Calendar"
 msgstr "Calendrier"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:338
+#: lib/Jifty/View/Declare/CRUD.pm:419
 msgid "Cancel"
 msgstr "Annuler"
 
@@ -214,44 +229,48 @@
 msgid "Close window"
 msgstr "Fermer la fenêtre"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:382
+#:
 msgid "Confirm delete?"
-msgstr "Confirmer l'effacement ?"
+msgstr "Confirmer la suppresion ?"
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm:97
+#: lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm:94
 msgid "Confirmation resent."
 msgstr "Confirmation transmise."
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:213
+#: lib/Jifty/Plugin/OpenID/View.pm:65
+msgid "Continue"
+msgstr "Continuer"
+
+#: lib/Jifty/View/Declare/.CRUD.pm.swp:17 lib/Jifty/View/Declare/CRUD.pm:675
 msgid "Create"
 msgstr "Enregistrer"
 
-#: lib/Jifty/Action/Record/Create.pm:81
+#: lib/Jifty/Action/Record/Create.pm:91
 #. (ref($record)
 msgid "Create of %1 failed: %2"
 msgstr "La création de %1 a échoué : %2"
 
-#: lib/Jifty/Action/Record/Create.pm:105
+#: lib/Jifty/Action/Record/Create.pm:118
 msgid "Created"
-msgstr "Créé"
+msgstr "Création réussie"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:415
+#:
 msgid "Database Administration"
 msgstr "Administration de la Base de Données"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:380
+#: lib/Jifty/View/Declare/CRUD.pm:429
 msgid "Delete"
 msgstr "Effacer"
 
-#: lib/Jifty/Action/Record/Delete.pm:76
+#: lib/Jifty/Action/Record/Delete.pm:82
 msgid "Deleted"
-msgstr "Effacé"
+msgstr "Suppression réussie"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:458 lib/Jifty/Plugin/AdminUI/View-not-yet.pm:486 lib/Jifty/Plugin/AdminUI/View-not-yet.pm:56
+#:
 msgid "Done?"
 msgstr "Fini ?"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:388 share/web/templates/__jifty/halo:126 share/web/templates/__jifty/halo:20
+#: lib/Jifty/View/Declare/CRUD.pm:349 share/web/templates/__jifty/halo:126 share/web/templates/__jifty/halo:20
 msgid "Edit"
 msgstr "Editer"
 
@@ -264,25 +283,25 @@
 msgid "Email"
 msgstr "Mail"
 
-#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:33
+#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:52
 msgid "Email address"
 msgstr "Adresse mail"
 
-#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:35
+#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:54
 msgid "Email address confirmed?"
 msgstr "Adresse mail confirmée ?"
 
-#: lib/Jifty/Action.pm:1158
+#: lib/Jifty/Action.pm:1312
 msgid "Foo cannot contain -, *, +, or ?."
 msgstr "Cette donnée ne doit pas contenir -, *, +, ou ?."
 
-#: lib/Jifty/Action.pm:1152
+#: lib/Jifty/Action.pm:1306
 msgid "Foo cannot contain uppercase letters."
 msgstr "Cette donnée ne doit pas contenir de majuscule."
 
-#: lib/Jifty/Action.pm:1136
+#: lib/Jifty/Action.pm:1285
 msgid "Foo values are always in lowercase."
-msgstr "Cett donnée doit toujours être en minuscule."
+msgstr "Cette donnée doit toujours être en minuscule."
 
 #: lib/Jifty/Plugin/ErrorTemplates/View.pm:27
 msgid "For one reason or another, you got to a web page that caused a bit of an error. And then you got to our 'basic' error handler. Which means we haven't written a pretty, easy to understand error message for you just yet. The message we do have is :"
@@ -292,52 +311,79 @@
 msgid "Go back home..."
 msgstr "Retour..."
 
+#: lib/Jifty/Plugin/OpenID/View.pm:35
+msgid "Go for it!"
+msgstr ""
+
 #: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:34
 msgid "Hashed Password"
 msgstr "Mot de passe crypté"
 
 #: lib/Jifty/Plugin/ErrorTemplates/View.pm:44
 msgid "Head on back home"
-msgstr ""
+msgstr "Retour accueil"
 
 #: lib/Jifty/I18N.pm:19 lib/Jifty/I18N.pm:23
 #. ('World')
 msgid "Hello, %1!"
 msgstr "Bonjour, %1"
 
-#: lib/Jifty/Plugin/SkeletonApp/View.pm:31 share/web/templates/_elements/sidebar:5
-#. ($u->$method()
+#: lib/Jifty/Plugin/Authentication/Facebook/Action/LoginFacebookUser.pm:103
+#. (Jifty->web->current_user->user_object->facebook_name)
+msgid "Hi %1!"
+msgstr "Bonjour %1"
+
+#: lib/Jifty/Plugin/I18N/Action/SetLang.pm:44
+#. (DateTime::Locale->load($lang)
+msgid "Hi, we speak %1."
+msgstr "Bonjour, langue disponible : %1."
+
+#: lib/Jifty/Plugin/SkeletonApp/View.pm:31 share/web/templates/_elements/sidebar:4
+#. ($u->username)
+#. (Jifty->web->current_user->username)
 msgid "Hiya, %1."
 msgstr "Bonjour, %1"
 
-#: lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm:23
+#: lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm:26
 msgid "Home"
-msgstr ""
+msgstr "Accueil"
 
-#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:30
+#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:49
 msgid "How should I display your name to other users?"
 msgstr "Nom affiché"
 
-#: lib/Jifty/Action.pm:1090
+#: lib/Jifty/Action.pm:1212
 msgid "I changed $field for you"
-msgstr ""
+msgstr "$field a été changé"
 
 #: lib/Jifty/Plugin/Authentication/Password/Action/ResetLostPassword.pm:64
 msgid "I'm not sure how this happened."
 msgstr ""
 
+#: lib/Jifty/Plugin/OpenID/View.pm:59
+msgid "If the username provided conflicts with an existing username or contains invalid characters, you will have to give us a new one."
+msgstr "Si ce nom d'utilisateur entre en conflit avec un nom existant ou contient des caractères non autorisés, vous devrez le changer."
+
 #: lib/Jifty/Plugin/ErrorTemplates/View.pm:62
 msgid "Internal error"
 msgstr "Erreur interne"
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm:78 lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm:79
+#: lib/Jifty/Plugin/OpenID/Action/AuthenticateOpenID.pm:59
+msgid "Invalid OpenID URL.  Please check to make sure it is correct.  (@{[$csr->err]})"
+msgstr "URL OpenID incorrecte. Vérifiez la. (@{[$csr->err]}) "
+
+#: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:63 lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm:75 lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm:79
 msgid "It doesn't look like there's an account by that name."
 msgstr "Il ne semble pas y avoir de compte sous ce nom."
 
-#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:89
+#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:106
 msgid "It looks like somebody else is using that address. Is there a chance you have another account?"
 msgstr "Cette adresse semble utilisée par quelqu'un d'autre. Vous avez peut-être un autre compte ?"
 
+#: lib/Jifty/Plugin/OpenID/Dispatcher.pm:43
+msgid "It looks like someone is already using that OpenID."
+msgstr "Cette URL OpenID est déjà utilisée par un autre utilisateur."
+
 #: lib/Jifty/Plugin/Authentication/Password/Action/Signup.pm:75
 msgid "It looks like you already have an account. Perhaps you want to <a href=\"/login\">log in</a> instead?"
 msgstr "Votre compte existe déjà . Vous voulez peut-être plutôt <a href=\"/login\">vous connecter</a> ?"
@@ -346,84 +392,115 @@
 msgid "It looks like you didn't enter the same password into both boxes. Give it another shot?"
 msgstr "Les mots de passe sont différents. Ré-essayer ?"
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm:82
+#: lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm:79
 msgid "It looks like you're already confirmed."
-msgstr "Votre adresse à déjà été confirmée."
+msgstr "Votre adresse a déjà été confirmée."
 
-#: lib/Jifty/View/Declare/Helpers.pm:375 share/web/templates/_elements/wrapper:18
+#: lib/Jifty/Plugin/I18N/Action/SetLang.pm:12
+msgid "Language"
+msgstr "Langue"
+
+#: lib/Jifty/View/Declare/Page.pm:194 share/web/templates/_elements/wrapper:18
 msgid "Loading..."
 msgstr "Chargement..."
 
-#: lib/Jifty/Plugin/Authentication/Password/Dispatcher.pm:106
+#: lib/Jifty/Plugin/Authentication/Password/Dispatcher.pm:132
 msgid "Login"
 msgstr "Identification"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:55
+#: lib/Jifty/Plugin/Authentication/Facebook/View.pm:24
+msgid "Login to Facebook now to get started!"
+msgstr "Connection à Facebook démarrée !"
+
+#: lib/Jifty/Plugin/Authentication/Facebook/View.pm:41
+msgid "Login to Facebook now to link it with your current account!"
+msgstr "Votre connection à Facebook doit maintenant être liée à votre compte courant !"
+
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:82
 msgid "Login with a password"
 msgstr "S'identifier avec un mot de passe"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:40
+#: lib/Jifty/Plugin/OpenID/View.pm:17
+msgid "Login with your OpenID"
+msgstr "S'identifier avec votre OpenID"
+
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:57
 msgid "Login!"
 msgstr "Identification"
 
-#: lib/Jifty/Plugin/Authentication/Password/Dispatcher.pm:123
+#: lib/Jifty/Plugin/Authentication/Password/Dispatcher.pm:151
 msgid "Logout"
 msgstr "Déconnection"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:63
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:90
 msgid "Lost your password?"
 msgstr "Vous avez oublié votre mot de passe ?"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:46 lib/Jifty/Plugin/AdminUI/View-not-yet.pm:473
-#. ($object_type)
-msgid "Manage records: [_1]"
-msgstr ""
-
 #: share/web/templates/__jifty/error/mason_internal_error:1
 msgid "Mason error"
 msgstr "Erreur Mason"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:429
+#:
 msgid "Models"
 msgstr "Modèles"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:81
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:114
 msgid "New password"
 msgstr "Nouveau mot de passe"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:172
+#: lib/Jifty/View/Declare/CRUD.pm:631
 msgid "Next Page"
 msgstr "Page Suivante"
 
-#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:29
+#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:48
 msgid "Nickname"
-msgstr "Surnom"
+msgstr "Identifiant"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:52
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:79
 msgid "No account yet? It's quick and easy. "
 msgstr "Pas encore de compte ? C'est facile et rapide. "
 
-#: lib/Jifty/Action/Record/Search.pm:130
+#: lib/Jifty/Action/Record/Search.pm:178
 msgid "No field contains"
 msgstr "Aucun champs contient"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:139
+#:
 msgid "No items found"
 msgstr "Aucun enregistrement"
 
-#: lib/Jifty/Web.pm:302
+#: lib/Jifty/View/Declare/CRUD.pm:550
+msgid "No items found."
+msgstr "Aucun enregistrement."
+
+#: lib/Jifty/Web.pm:308
 msgid "No request to handle"
 msgstr "Aucune requête à traiter"
 
+#: lib/Jifty/Plugin/UUID/Widget.pm:37
+msgid "No value yet"
+msgstr "Aucune réponse"
+
 #:
 msgid "Online Documentation"
 msgstr "Documentation en ligne"
 
 #: lib/Jifty/Plugin/OnlineDocs/Dispatcher.pm:26
 msgid "Online docs"
-msgstr ""
+msgstr "Documentation en ligne"
+
+#: lib/Jifty/Plugin/OpenID/Action/AuthenticateOpenID.pm:27
+msgid "OpenID URL"
+msgstr "URL OpenID"
+
+#: lib/Jifty/Plugin/OpenID/Action/VerifyOpenID.pm:54
+msgid "OpenID verification failed.  It looks like you cancelled the OpenID verification request."
+msgstr "Echec de la vérification OpenID. Il semblerait que vous avez annulé votre demande de vérification."
+
+#: lib/Jifty/Plugin/OpenID/Action/VerifyOpenID.pm:63
+msgid "OpenID verification failed: "
+msgstr "Echec de la vérification OpenID: "
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:134
+#: lib/Jifty/View/Declare/CRUD.pm:598
 #. ($page, $collection->pager->last_page)
 msgid "Page %1 of %2"
 msgstr "Page %1 / %2"
@@ -436,15 +513,15 @@
 msgid "Parent"
 msgstr ""
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:29 lib/Jifty/Plugin/Authentication/Password/Action/ResetLostPassword.pm:32 lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm:26
+#: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:29 lib/Jifty/Plugin/Authentication/Password/Action/ResetLostPassword.pm:32 lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm:59
 msgid "Password"
 msgstr "Mot de passe"
 
-#: lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm:88
+#: lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm:134
 msgid "Passwords need to be at least six characters long"
 msgstr "Il faut au minimum six caractères"
 
-#: lib/Jifty/Record.pm:272 lib/Jifty/Record.pm:351 lib/Jifty/Record.pm:70
+#: lib/Jifty/Record.pm:505 lib/Jifty/Record.pm:550 lib/Jifty/Record.pm:70
 msgid "Permission denied"
 msgstr "Autorisation refusée"
 
@@ -452,13 +529,17 @@
 msgid "Please email us!"
 msgstr "Envoyez nous un mail !"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:163
+#: lib/Jifty/View/Declare/CRUD.pm:620
 msgid "Previous Page"
 msgstr "Page précédente"
 
+#: lib/Jifty/View/Declare/CRUD.pm:432
+msgid "Really delete?"
+msgstr "Confirmer l'effacement ?"
+
 #: lib/Jifty/Plugin/Authentication/Password/Action/ResetLostPassword.pm:65
 msgid "Really, really sorry."
-msgstr "Vraiment désolé"
+msgstr "Vraiment désolé."
 
 #:
 msgid "Record created"
@@ -473,11 +554,11 @@
 msgid "Rendered in %1s"
 msgstr ""
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:78
+#:
 msgid "Reset lost password"
 msgstr "Changement de mot de passe"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:53
+#:
 msgid "Run the action"
 msgstr "Exécuter l'action"
 
@@ -485,7 +566,7 @@
 msgid "SQL Statements"
 msgstr ""
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:327
+#: lib/Jifty/View/Declare/CRUD.pm:410
 msgid "Save"
 msgstr "Enregistrer"
 
@@ -493,31 +574,35 @@
 msgid "Schema"
 msgstr "Schéma"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:304
+#: lib/Jifty/View/Declare/CRUD.pm:298
 msgid "Search"
 msgstr "Chercher"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:119
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:180
 msgid "Send"
 msgstr "Enregistrer"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:97
+#:
 msgid "Send a link to reset your password"
 msgstr "Recevoir un lien pour changer votre mot de passe"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:109 lib/Jifty/Plugin/Authentication/Password/View.pm:114
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:175
 msgid "Send a password reminder"
 msgstr ""
 
-#: lib/Jifty/Plugin/Authentication/Password/Dispatcher.pm:111
+#: lib/Jifty/Plugin/Feedback/View.pm:21
+msgid "Send us feedback!"
+msgstr "Envoyez nous votre avis !"
+
+#: lib/Jifty/Plugin/Authentication/Password/Dispatcher.pm:137 lib/Jifty/Plugin/Authentication/Password/View.pm:42 lib/Jifty/Plugin/Authentication/Password/View.pm:47
 msgid "Sign up"
 msgstr "S'enregistrer"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:53
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:80
 msgid "Sign up for an account!"
 msgstr "Enregistrez vous pour avoir un compte !"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:30 lib/Jifty/Plugin/Authentication/Password/View.pm:35
+#:
 msgid "Signup"
 msgstr "Enregistrement"
 
@@ -538,32 +623,45 @@
 msgid "Sorry about this."
 msgstr "Désolé."
 
+#: lib/Jifty/Plugin/Authentication/Facebook/Action/LoginFacebookUser.pm:62
+msgid "Sorry, something weird happened (we couldn't create a user for you).  Try again later."
+msgstr "Désolé, un problème nous empêche de créer cet utilisateur. Ré-essayer plus tard."
+
 #:
 msgid "Table of Contents"
 msgstr "Table des matières"
 
-#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:82
+#: lib/Jifty/Plugin/Feedback/View.pm:23
+#. (Jifty->config->framework('ApplicationName')
+msgid "Tell us what's good, what's bad, and what else you want %1 to do!"
+msgstr "Dites nous ce qui vous plaît, vous déplaît, ou tout autre avis %1"
+
+#: lib/Jifty/Plugin/User/Mixin/Model/User.pm:99
 #. ($new_email)
 msgid "That %1 doesn't look like an email address."
 msgstr "%1 ne ressemble pas à une adresse mail."
 
-#: lib/Jifty/Action.pm:878
+#: lib/Jifty/Action.pm:975
 msgid "That doesn't look like a correct value"
 msgstr "Valeur incorecte"
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm:71 lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm:72 lib/Jifty/Plugin/Authentication/Password/Action/Signup.pm:70
+#: lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm:68 lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm:72 lib/Jifty/Plugin/Authentication/Password/Action/Signup.pm:70
 msgid "That doesn't look like an email address."
 msgstr "Cela ne ressemble pas à une adresse mail."
 
-#: lib/Jifty/Action/Record.pm:249
+#: lib/Jifty/Action/Record.pm:304
 msgid "That doesn't look right, but I don't know why"
 msgstr "Problème"
 
-#: lib/Jifty/Action/Record.pm:181
+#: lib/Jifty/Plugin/OpenID/Dispatcher.pm:48
+msgid "The OpenID '$openid' has been linked to your account."
+msgstr "L'openID '$openid' a été attaché à votre compte."
+
+#: lib/Jifty/Action/Record.pm:205
 msgid "The passwords you typed didn't match each other"
 msgstr "Les mots de passe tapés ne sont pas identiques"
 
-#: lib/Jifty/Web.pm:365
+#: lib/Jifty/Web.pm:429
 msgid "There was an error completing the request.  Please try again later."
 msgstr "Problème lors de l'exécution de cette requête. Ré-essayez plus tard."
 
@@ -573,17 +671,17 @@
 
 #: lib/Jifty/Plugin/ErrorTemplates/View.pm:37
 msgid "There's a pretty good chance that error message doesn't mean anything to you, but we'd rather you have a little bit of information about what went wrong than nothing. We've logged this error, so we know we need to write something friendly explaining just what happened and how to fix it."
-msgstr ""
+msgstr "Il y a de fortes chances pour que ce message d'erreur ne veuille rien dire pour vous, mais nous préférons vous afficher ce laïus plutôt que rien. Nous avons enregistré cette erreur, nous n'avons plus qu'à écrire un message clair pour vous expliquer le problème et comment le régler. "
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:419
+#:
 msgid "This console lets you manage the records in your Jifty database. Below, you should see a list of all your database tables. Feel free to go through and add, delete or modify records."
 msgstr "Cette page vous permet de gérer vos enregistrements dans la base. Vous trouverez ci-dessous la listes des tables. Vous pouvez ajouter, effacer ou modifier les enregistements."
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:425
+#:
 msgid "To disable this administrative console, add \"AdminMode: 0\" under the \"framework:\" settings in the config file (etc/config.yml)."
 msgstr "Pour désactiver cette console d'administration, ajoutez \"AdminMode: 0\" dans la rubrique \"framework:\" du fichier de configuration (etc/config.yml)."
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:127
+#: lib/Jifty/View/Declare/CRUD.pm:518
 msgid "Toggle search"
 msgstr "Rechercher"
 
@@ -599,7 +697,7 @@
 msgid "Type that again?"
 msgstr "Confirmez"
 
-#: lib/Jifty/Action/Record/Update.pm:156
+#: lib/Jifty/Action/Record/Update.pm:170
 msgid "Updated"
 msgstr "Modification"
 
@@ -611,6 +709,10 @@
 msgid "W00t"
 msgstr ""
 
+#: lib/Jifty/Plugin/OpenID/View.pm:53
+msgid "We need you to set a username or quickly check the one associated with your OpenID. Your username is what other people will see when you ask questions or make suggestions"
+msgstr "Vous devez préciser un nom d'utilisateur ou vérifier celui associé à votre OpenID. Votre nom d'utilisateur sera présenté lorsque rédigez un commentaire"
+
 #: lib/Jifty/Plugin/Authentication/Password/Action/Signup.pm:114
 msgid "We've sent a confirmation message to your email box."
 msgstr "Nous vous avons envoyé un mail de confirmation."
@@ -620,7 +722,11 @@
 msgid "Welcome %1 to the %2"
 msgstr "Bienvenue %1 sur %2"
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:181
+#: lib/Jifty/Plugin/OpenID/Dispatcher.pm:64
+msgid "Welcome back, "
+msgstr "Re-bonjour, "
+
+#: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:182
 #. ($user->name)
 msgid "Welcome back, %1."
 msgstr "Re-bonjour, %1."
@@ -630,13 +736,12 @@
 msgid "Welcome to %1!"
 msgstr "Bienvenue sur %1 !"
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/Signup.pm:113
+#: lib/Jifty/Plugin/Authentication/Password/Action/ConfirmEmail.pm:57 lib/Jifty/Plugin/Authentication/Password/Action/Signup.pm:113
 #. (Jifty->config->framework('ApplicationName')
 msgid "Welcome to %1, %2."
 msgstr "Bienvenue sur %1, %2."
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/ConfirmEmail.pm:57
-#. (Jifty->config->framework('ApplicationName')
+#:
 msgid "Welcome to %1, %2. "
 msgstr "Bienvenue sur %1, %2. "
 
@@ -644,6 +749,14 @@
 msgid "Welcome to your new Jifty application"
 msgstr "Bienvenue sur votre nouvelle application Jifty"
 
+#: lib/Jifty/Plugin/OpenID/Action/CreateOpenIDUser.pm:104
+msgid "Welcome, "
+msgstr "Bienvenue, "
+
+#: lib/Jifty/Plugin/OpenID/View.pm:42
+msgid "You already logged in."
+msgstr "Vous êtes déjà connecté."
+
 #: lib/Jifty/Plugin/Authentication/Password/Action/ResetLostPassword.pm:63
 msgid "You don't exist."
 msgstr "Votre compte n'existe pas."
@@ -660,23 +773,27 @@
 msgid "You have already confirmed your account."
 msgstr "Votre compte à déjà été confirmé."
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:98
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:146
 msgid "You lost your password. A link to reset it will be sent to the following email address:"
 msgstr "Vous avez perdu votre mot de passe. Un lien pour changer ce mot de passe sera envoyé à l'adresse suivante :"
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:115
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:176
 msgid "You lost your password. A reminder will be send to the following mail:"
 msgstr "Vous avez perdu votre mot de passe. Un lien pour changer ce mot de passe sera envoyé à l'adresse suivante :"
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:152
+#:
 msgid "You may have mistyped your email address or password. Give it another shot."
 msgstr "Vous vous êtes trompé dans votre mail ou votre mot de passe. Ré-essayez."
 
-#: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:146
+#: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:146 lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:152
 msgid "You may have mistyped your email or password. Give it another shot."
 msgstr "Vous vous êtes trompé dans votre mail ou votre mot de passe. Ré-essayez."
 
-#: lib/Jifty/Action.pm:865
+#: lib/Jifty/Plugin/Authentication/Facebook/Action/LinkFacebookUser.pm:40
+msgid "You must be logged in to link your user to your Facebook account."
+msgstr "Vous devez être connecté pour lier votre compte d'utilisateur à votre compte Facebook."
+
+#: lib/Jifty/Action.pm:962
 msgid "You need to fill in this field"
 msgstr "Information obligatoire"
 
@@ -685,14 +802,19 @@
 msgid "You said you wanted a pony. (Source %1)"
 msgstr ""
 
-#: lib/Jifty/Plugin/Authentication/Password/View.pm:69
+#: lib/Jifty/Plugin/Authentication/Password/View.pm:96
 msgid "You're already logged in."
 msgstr "Vous êtes déjà connecté."
 
-#: lib/Jifty/Plugin/SkeletonApp/View.pm:34 share/web/templates/_elements/sidebar:7
+#: lib/Jifty/Plugin/SkeletonApp/View.pm:34 share/web/templates/_elements/sidebar:6
 msgid "You're not currently signed in."
 msgstr "Vous n'êtes pas encore connecté."
 
+#: lib/Jifty/Plugin/Authentication/Facebook/Action/LinkFacebookUser.pm:72
+#. (Jifty->web->current_user->user_object->facebook_name)
+msgid "Your account has been successfully linked to your Facebook user %1!"
+msgstr "Votre compte a bien été lié à votre compte Facebook %1"
+
 #: lib/Jifty/Plugin/Authentication/Password/Action/Login.pm:38
 msgid "Your browser can remember your login for you"
 msgstr "Votre navigateur peut mémoriser cette identification"
@@ -701,18 +823,10 @@
 msgid "Your password has been reset.  Welcome back."
 msgstr "Votre mot de passe à été changé."
 
-#: lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm:28
+#: lib/Jifty/Plugin/Authentication/Password/Mixin/Model/User.pm:61
 msgid "Your password should be at least six characters"
 msgstr "Votre mot de passe doit contenir au moins six caractères"
 
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:246
-msgid "asc"
-msgstr ""
-
-#: lib/Jifty/Plugin/AdminUI/View-not-yet.pm:264
-msgid "desc"
-msgstr ""
-
 #: lib/Jifty/Plugin/Authentication/Password/Action/SendAccountConfirmation.pm:31
 msgid "email address"
 msgstr "adresse mail"
@@ -721,6 +835,10 @@
 msgid "for now, and try to forget that we let you down."
 msgstr ""
 
+#: lib/Jifty/Action/Record.pm:253
+msgid "no value"
+msgstr ""
+
 #: lib/Jifty/Manual/PageRegions.pod:188
 msgid "text of the link"
 msgstr ""
@@ -733,6 +851,6 @@
 msgid "type your password again"
 msgstr "confirmez votre mot de passe"
 
-#: lib/Jifty/Action.pm:1050
+#: lib/Jifty/Action.pm:1172
 msgid "warning"
 msgstr ""

Modified: jifty/branches/virtual-models/share/web/static/js/jifty.js
==============================================================================
--- jifty/branches/virtual-models/share/web/static/js/jifty.js	(original)
+++ jifty/branches/virtual-models/share/web/static/js/jifty.js	Sat Dec  1 17:17:34 2007
@@ -24,7 +24,7 @@
 }
 
 function _get_onclick(action_hash, name, args, path) {
-    var onclick = 'if(event.ctrlKey||event.metaKey||event.altKey||event.shiftKey) return true; return update('
+    var onclick = 'if(event.ctrlKey||event.metaKey||event.altKey||event.shiftKey) return true; return Jifty.update('
     + JSON.stringify({'continuation': {},
                       'actions': action_hash,
                       'fragments': [{'mode': 'Replace', 'args': args, 'region': name, 'path': path}]})
@@ -519,9 +519,9 @@
     getAction: function (element) {
         element = $(element);    
         var moniker = Form.Element.getMoniker(element);
-        if (!current_actions[moniker])
-            current_actions[moniker] = new Action(moniker);
-        return current_actions[moniker];
+        if (!current_actions.get(moniker))
+            current_actions.set(moniker, new Action(moniker));
+        return current_actions.get(moniker);
     },
 
     // Returns the name of the field
@@ -609,14 +609,14 @@
         var pairs = element.getAttribute("name").split("|");
         for (var i = 0; i < pairs.length; i++) {
             var bits = pairs[i].split('=',2);
-            extras[bits[0]] = bits[1];
+            extras.set(bits[0], bits[1]);
         }
         return extras;
     },
 
     buttonActions: function(element) {
         element = $(element);
-        var actions = Form.Element.buttonArguments(element)['J:ACTIONS'];
+        var actions = Form.Element.buttonArguments(element).get('J:ACTIONS');
         if(actions) {
             return actions.split(",");
         } else {
@@ -634,7 +634,7 @@
             var e = document.createElement("input");
             e.setAttribute("type", "hidden");
             e.setAttribute("name", keys[i]);
-            e.setAttribute("value", args[keys[i]]);
+            e.setAttribute("value", args.get(keys[i]));
             e['virtualform'] = Form.Element.getForm(element);
             extras.push(e);
         }
@@ -737,8 +737,8 @@
         this.name = name;
         this.args = $H(args);
         this.path = path;
-        this.parent = parent ? fragments[parent] : null;
-        if (fragments[name]) {
+        this.parent = parent ? fragments.get(parent) : null;
+        if (fragments.get(name)) {
             // If this fragment already existed, we want to wipe out
             // whatever evil lies we might have said earlier; do this
             // by clearing out everything that looks relevant
@@ -747,12 +747,12 @@
                 var k = keys[i];
                 var parsed = k.match(/^(.*?)\.(.*)/);
                 if ((parsed != null) && (parsed.length == 3) && (parsed[1] == this.name)) {
-                    delete current_args[k];
+                    current_args.unset(k);
                 }
             }
         }
 
-        fragments[name] = this;
+        fragments.set(name, this);
     },
 
     setPath: function(supplied) {
@@ -761,7 +761,7 @@
         for (var i = 0; i < keys.length; i++) {
             var k = keys[i];
             if (k == this.name) {
-                this.path = current_args[k];
+                this.path = current_args.get(k);
             }
         }
 
@@ -771,7 +771,7 @@
         }
         
         // Propagate back to current args
-        current_args[this.name] = this.path;
+        current_args.set(this.name, this.path);
 
         // Return new value
         return this.path;
@@ -785,7 +785,7 @@
             var k = keys[i];
             var parsed = k.match(/^(.*?)\.(.*)/);
             if ((parsed != null) && (parsed.length == 3) && (parsed[1] == this.name)) {
-                this.args[parsed[2]] = current_args[k];
+                this.args.set(parsed[2], current_args.get(k));
             }
         }
 
@@ -796,7 +796,7 @@
         keys = supplied.keys();
         for (var i = 0; i < keys.length; i++) {
             var k = keys[i];
-            current_args[this.name+'.'+k] = supplied[k];
+            current_args.set(this.name+'.'+k, supplied.get(k));
         }
         
         // Return new values
@@ -854,12 +854,12 @@
         // If we're removing the element, do it now
         // XXX TODO: Effects on this?
         if (f['mode'] == "Delete") {
-            fragments[name] = null;
+            fragments.set(name, null);
             Element.remove(element);
             return;
         }
 
-        f['is_new'] = (fragments[name] ? false : true);
+        f['is_new'] = (fragments.get(name) ? false : true);
         // If it's new, we need to create it so we can dump it
         if (f['is_new']) {
             // Find what region we're inside
@@ -878,14 +878,14 @@
 
             // Make the region (for now)
             new Region(name, f['args'], f['path'], f['parent']);
-        } else if ((f['path'] != null) && f['toggle'] && (f['path'] == fragments[name].path)) {
+        } else if ((f['path'] != null) && f['toggle'] && (f['path'] == fragments.get(name).path)) {
             // If they set the 'toggle' flag, and clicking wouldn't change the path
             Element.update(element, '');
-            fragments[name].path = null;
+            fragments.get(name).path = null;
             return;
         } else if (f['path'] == null) {
             // If they didn't know the path, fill it in now
-            f['path'] == fragments[name].path;
+            f['path'] == fragments.get(name).path;
         }
 
     return f;    
@@ -931,7 +931,7 @@
 //   - f: fragment spec
 var apply_fragment_updates = function(fragment, f) {
     // We found the right fragment
-    var dom_fragment = fragments[f['region']];
+    var dom_fragment = fragments.get(f['region']);
     var new_dom_args = $H();
 
     var element = f['element'];
@@ -947,7 +947,7 @@
             } else if (fragment_bit.firstChild) {
                 textContent = fragment_bit.firstChild.nodeValue;
             }
-            new_dom_args[fragment_bit.getAttribute("name")] = textContent;
+            new_dom_args.set(fragment_bit.getAttribute("name"), textContent);
         },
       content: function(fragment_bit) {
             var textContent = '';
@@ -1001,7 +1001,7 @@
 //     - 'element' is the CSS selector of the element to update, if 'region' isn't supplied
 //     - 'mode' is one of 'Replace', or the name of a Prototype Insertion
 //     - 'effect' is the name of a Prototype Effect
-function update() {
+Jifty.update = function () {
     // loads
     if(!Ajax.getTransport()) return true;
     // XXX: prevent default behavior in IE
@@ -1019,7 +1019,7 @@
     var disabled_elements = $A();
 
     // Set request base path
-    request['path'] = '/__jifty/webservices/xml';
+    request.set('path', '/__jifty/webservices/xml');
 
     // Grab extra arguments (from a button)
     var button_args = Form.Element.buttonFormElements(trigger);
@@ -1039,11 +1039,11 @@
         optional_fragments = [ prepare_element_for_update({'mode':'Replace','args':{},'region':'__page','path': null}) ];
     // Build actions structure
     var has_request = 0;
-    request['actions'] = $H();
+    request.set('actions', $H());
     for (var moniker in named_args['actions']) {
         var disable = named_args['actions'][moniker];
         var a = new Action(moniker, button_args);
-            current_actions[moniker] = a; // XXX: how do i make this bloody singleton?
+            current_actions.set(moniker, a); // XXX: how do i make this bloody singleton?
         // Special case for Redirect, allow optional, implicit __page
         // from the response to be used.
         if (a.actionClass == 'Jifty::Action::Redirect')
@@ -1066,13 +1066,13 @@
                     fields[argname] = { value: override[argname] };
                 }
             }
-            request['actions'][moniker] = param;
+            request.get('actions').set(moniker, param);
             ++has_request;
         }
 
     }
 
-    request['fragments'] = $H();
+    request.set('fragments', $H());
     var update_from_cache = new Array;
 
     // Build fragments structure
@@ -1087,7 +1087,7 @@
             var content_node = document.createElement('content');
             var cached_result;
 
-            Jifty.Web.current_region = fragments[f['region']];
+            Jifty.Web.current_region = fragments.get(f['region']);
             try { cached_result = apply_cached_for_action(cached['content'], []) }
             catch (e) { alert(e) }
 
@@ -1107,7 +1107,7 @@
             my_fragment.setAttribute('id', f['region']);
             update_from_cache.push(function(){
                     var cached_result;
-                    Jifty.Web.current_region = fragments[f['region']];
+                    Jifty.Web.current_region = fragments.get(f['region']);
                     try {
                         cached_result = apply_cached_for_action(cached['content'], Form.getActions(form));
                     }
@@ -1134,14 +1134,14 @@
 
         // Update with all new values
         var name = f['region'];
-        var fragment_request = fragments[name].data_structure(f['path'], f['args']);
+        var fragment_request = fragments.get(name).data_structure(f['path'], f['args']);
 
         if (f['is_new'])
             // Ask for the wrapper if we are making a new region
             fragment_request['wrapper'] = 1;
 
         // Push it onto the request stack
-        request['fragments'][name] = fragment_request;
+        request.get('fragments').set(name, fragment_request);
         ++has_request;
     }
 
@@ -1169,7 +1169,7 @@
                                       var text = error.textContent
                                           ? error.textContent
                                           : (error.firstChild ? error.firstChild.nodeValue : '');
-                                      var action = current_actions[moniker];
+                                      var action = current_actions.get(moniker);
                                       action.result.field_error[field.getAttribute("name")] = text;
                                       }
                               }});
@@ -1226,15 +1226,15 @@
     };
 
     // Build variable structure
-    request['variables'] = $H();
+    request.set('variables', $H());
     var keys = current_args.keys();
     for (var i = 0; i < keys.length; i++) {
         var k = keys[i];
-        request['variables']['region-'+k] = current_args[k];
+        request.get('variables').set('region-'+k, current_args.get(k));
     }
 
     // Build continuation structure
-    request['continuation'] = named_args['continuation'];
+    request.set('continuation', named_args['continuation']);
 
     // Push any state variables which we set into the forms
     for (var i = 0; i < document.forms.length; i++) {
@@ -1243,20 +1243,20 @@
         for (var j = 0; j < keys.length; j++) {
             var n = keys[j];
             if (form['J:V-region-'+n]) {
-                form['J:V-region-'+n].value = current_args[n];
+                form['J:V-region-'+n].value = current_args.get(n);
             } else {
                 var hidden = document.createElement('input');
                 hidden.setAttribute('type',  'hidden');
                 hidden.setAttribute('name',  'J:V-region-'+n);
                 hidden.setAttribute('id',    'J:V-region-'+n);
-                hidden.setAttribute('value', current_args[n]);
+                hidden.setAttribute('value', current_args.get(n));
                 form.appendChild(hidden);
             }
         }
     }
 
     // Set up our options
-    var options = { postBody: JSON.stringify(request),
+    var options = { postBody: request.toJSON(), //JSON.stringify(request.toObject),
                     onSuccess: onSuccess,
                     onException: onFailure,
                     onFailure: onFailure,
@@ -1271,6 +1271,11 @@
     return false;
 }
 
+function update ( named_args, trigger ) {
+    alert( 'please use Jifty.update instead of update.' );
+    return Jifty.update( named_args, trigger );
+}
+
 function trace( msg ){
   if( typeof( jsTrace ) != 'undefined' ){
     jsTrace.send( msg );
@@ -1411,10 +1416,10 @@
   getUpdatedChoices: function() {
       var request = { path: this.url, actions: {} };
 
-      var a = $H();
+      var a = {}; //$H();
       a['moniker'] = 'autocomplete';
       a['class']   = 'Jifty::Action::Autocomplete';
-      a['fields']  = $H();
+      a['fields']  = {}; //$H();
       a['fields']['moniker']  = this.action.moniker;
       a['fields']['argument'] = Form.Element.getField(this.field);
       request['actions']['autocomplete'] = a;
@@ -1515,33 +1520,39 @@
     // and then merge them.
     var hiddens = $H();
     var buttons = $H();
-    var inputs = $H()
+    var inputs = $H();
     for (var i = 0; i < elements.length; i++) {
         var e = elements[i];
         var parsed = e.getAttribute("name").match(/^J:V-region-__page\.(.*)/);
         var extras = Form.Element.buttonArguments(e);
-        if (extras.keys().length > 0) {
+        if (extras.keys().length > 1) {
             // Button with values
             for (var j = 0; j < extras.keys().length; j++) {
+                if ( extras.keys()[j] == 'extend' ) continue;
                 // Might also have J:V mappings on it
                 parsed = extras.keys()[j].match(/^J:V-region-__page\.(.*)/);
                 if ((parsed != null) && (parsed.length == 2)) {
-                    buttons[parsed[1]] = extras.values()[j];
+                    buttons.set(parsed[1], extras.values()[j]);
                 } else if (extras.keys()[j].length > 0) {
-                    inputs[extras.keys()[j]] = extras.values()[j];
+                    inputs.set(extras.keys()[j], extras.values()[j]);
                 }
                 
             }
         } else if ((parsed != null) && (parsed.length == 2)) {
             // Hidden default
-            hiddens[parsed[1]] = $F(e);
+            hiddens.set(parsed[1], $F(e));
         } else if (e.name.length > 0) {
             // Straight up values
-            inputs[e.name] = $F(e);
+            inputs.set(e.name, $F(e));
         }
     }
 
     var args = hiddens.merge(buttons.merge(inputs));
+
+    /* we want to feed a common object instead of a Hash to Jifty.update */ 
+    var args_object = {};
+    args.each( function( pair ) { args_object[pair.key] = pair.value; } );
+
     if(event.ctrlKey||event.metaKey||event.altKey||event.shiftKey) return true;
-    return update( {'continuation':{},'actions':null,'fragments':[{'mode':'Replace','args':args,'region':'__page','path': submit_to}]}, elt );
+    return Jifty.update( {'continuation':{},'actions':null,'fragments':[{'mode':'Replace','args':args_object,'region':'__page','path': submit_to}]}, elt );
 }

Modified: jifty/branches/virtual-models/share/web/static/js/prototype.js
==============================================================================
--- jifty/branches/virtual-models/share/web/static/js/prototype.js	(original)
+++ jifty/branches/virtual-models/share/web/static/js/prototype.js	Sat Dec  1 17:17:34 2007
@@ -1,102 +1,290 @@
-/*  Prototype JavaScript framework, version 1.4.0
- *  (c) 2005 Sam Stephenson <sam at conio.net>
- *
- *  THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff
- *  against the source tree, available from the Prototype darcs repository.
+/*  Prototype JavaScript framework, version 1.6.0
+ *  (c) 2005-2007 Sam Stephenson
  *
  *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://www.prototypejs.org/
  *
- *  For details, see the Prototype web site: http://prototype.conio.net/
- *
-/*--------------------------------------------------------------------------*/
+ *--------------------------------------------------------------------------*/
 
 var Prototype = {
-  Version: '1.4.0',
-  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
+  Version: '1.6.0',
 
-  emptyFunction: function() {},
-  K: function(x) {return x}
-}
+  Browser: {
+    IE:     !!(window.attachEvent && !window.opera),
+    Opera:  !!window.opera,
+    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
+    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+  },
+
+  BrowserFeatures: {
+    XPath: !!document.evaluate,
+    ElementExtensions: !!window.HTMLElement,
+    SpecificElementExtensions:
+      document.createElement('div').__proto__ &&
+      document.createElement('div').__proto__ !==
+        document.createElement('form').__proto__
+  },
+
+  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+  emptyFunction: function() { },
+  K: function(x) { return x }
+};
 
+if (Prototype.Browser.MobileSafari)
+  Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+if (Prototype.Browser.WebKit)
+  Prototype.BrowserFeatures.XPath = false;
+
+/* Based on Alex Arnell's inheritance implementation. */
 var Class = {
   create: function() {
-    return function() {
+    var parent = null, properties = $A(arguments);
+    if (Object.isFunction(properties[0]))
+      parent = properties.shift();
+
+    function klass() {
       this.initialize.apply(this, arguments);
     }
+
+    Object.extend(klass, Class.Methods);
+    klass.superclass = parent;
+    klass.subclasses = [];
+
+    if (parent) {
+      var subclass = function() { };
+      subclass.prototype = parent.prototype;
+      klass.prototype = new subclass;
+      parent.subclasses.push(klass);
+    }
+
+    for (var i = 0; i < properties.length; i++)
+      klass.addMethods(properties[i]);
+
+    if (!klass.prototype.initialize)
+      klass.prototype.initialize = Prototype.emptyFunction;
+
+    klass.prototype.constructor = klass;
+
+    return klass;
+  }
+};
+
+Class.Methods = {
+  addMethods: function(source) {
+    var ancestor   = this.superclass && this.superclass.prototype;
+    var properties = Object.keys(source);
+
+    if (!Object.keys({ toString: true }).length)
+      properties.push("toString", "valueOf");
+
+    for (var i = 0, length = properties.length; i < length; i++) {
+      var property = properties[i], value = source[property];
+      if (ancestor && Object.isFunction(value) &&
+          value.argumentNames().first() == "$super") {
+        var method = value, value = Object.extend((function(m) {
+          return function() { return ancestor[m].apply(this, arguments) };
+        })(property).wrap(method), {
+          valueOf:  function() { return method },
+          toString: function() { return method.toString() }
+        });
+      }
+      this.prototype[property] = value;
+    }
+
+    return this;
   }
-}
+};
 
-var Abstract = new Object();
+var Abstract = { };
 
 Object.extend = function(destination, source) {
-  for (property in source) {
+  for (var property in source)
     destination[property] = source[property];
-  }
   return destination;
-}
+};
 
-Object.inspect = function(object) {
-  try {
-    if (object == undefined) return 'undefined';
-    if (object == null) return 'null';
-    return object.inspect ? object.inspect() : object.toString();
-  } catch (e) {
-    if (e instanceof RangeError) return '...';
-    throw e;
-  }
-}
+Object.extend(Object, {
+  inspect: function(object) {
+    try {
+      if (object === undefined) return 'undefined';
+      if (object === null) return 'null';
+      return object.inspect ? object.inspect() : object.toString();
+    } catch (e) {
+      if (e instanceof RangeError) return '...';
+      throw e;
+    }
+  },
 
-Function.prototype.bind = function() {
-  var __method = this, args = $A(arguments), object = args.shift();
-  return function() {
-    return __method.apply(object, args.concat($A(arguments)));
-  }
-}
+  toJSON: function(object) {
+    var type = typeof object;
+    switch (type) {
+      case 'undefined':
+      case 'function':
+      case 'unknown': return;
+      case 'boolean': return object.toString();
+    }
+
+    if (object === null) return 'null';
+    if (object.toJSON) return object.toJSON();
+    if (Object.isElement(object)) return;
+
+    var results = [];
+    for (var property in object) {
+      var value = Object.toJSON(object[property]);
+      if (value !== undefined)
+        results.push(property.toJSON() + ': ' + value);
+    }
+
+    return '{' + results.join(', ') + '}';
+  },
+
+  toQueryString: function(object) {
+    return $H(object).toQueryString();
+  },
+
+  toHTML: function(object) {
+    return object && object.toHTML ? object.toHTML() : String.interpret(object);
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({ }, object);
+  },
+
+  isElement: function(object) {
+    return object && object.nodeType == 1;
+  },
+
+  isArray: function(object) {
+    return object && object.constructor === Array;
+  },
+
+  isHash: function(object) {
+    return object instanceof Hash;
+  },
+
+  isFunction: function(object) {
+    return typeof object == "function";
+  },
 
-Function.prototype.bindAsEventListener = function(object) {
-  var __method = this;
-  return function(event) {
-    return __method.call(object, event || window.event);
+  isString: function(object) {
+    return typeof object == "string";
+  },
+
+  isNumber: function(object) {
+    return typeof object == "number";
+  },
+
+  isUndefined: function(object) {
+    return typeof object == "undefined";
   }
-}
+});
 
-Object.extend(Number.prototype, {
-  toColorPart: function() {
-    var digits = this.toString(16);
-    if (this < 16) return '0' + digits;
-    return digits;
+Object.extend(Function.prototype, {
+  argumentNames: function() {
+    var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");
+    return names.length == 1 && !names[0] ? [] : names;
   },
 
-  succ: function() {
-    return this + 1;
+  bind: function() {
+    if (arguments.length < 2 && arguments[0] === undefined) return this;
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function() {
+      return __method.apply(object, args.concat($A(arguments)));
+    }
   },
 
-  times: function(iterator) {
-    $R(0, this, true).each(iterator);
-    return this;
+  bindAsEventListener: function() {
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function(event) {
+      return __method.apply(object, [event || window.event].concat(args));
+    }
+  },
+
+  curry: function() {
+    if (!arguments.length) return this;
+    var __method = this, args = $A(arguments);
+    return function() {
+      return __method.apply(this, args.concat($A(arguments)));
+    }
+  },
+
+  delay: function() {
+    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+    return window.setTimeout(function() {
+      return __method.apply(__method, args);
+    }, timeout);
+  },
+
+  wrap: function(wrapper) {
+    var __method = this;
+    return function() {
+      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
+    }
+  },
+
+  methodize: function() {
+    if (this._methodized) return this._methodized;
+    var __method = this;
+    return this._methodized = function() {
+      return __method.apply(null, [this].concat($A(arguments)));
+    };
   }
 });
 
+Function.prototype.defer = Function.prototype.delay.curry(0.01);
+
+Date.prototype.toJSON = function() {
+  return '"' + this.getUTCFullYear() + '-' +
+    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+    this.getUTCDate().toPaddedString(2) + 'T' +
+    this.getUTCHours().toPaddedString(2) + ':' +
+    this.getUTCMinutes().toPaddedString(2) + ':' +
+    this.getUTCSeconds().toPaddedString(2) + 'Z"';
+};
+
 var Try = {
   these: function() {
     var returnValue;
 
-    for (var i = 0; i < arguments.length; i++) {
+    for (var i = 0, length = arguments.length; i < length; i++) {
       var lambda = arguments[i];
       try {
         returnValue = lambda();
         break;
-      } catch (e) {}
+      } catch (e) { }
     }
 
     return returnValue;
   }
-}
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
 
 /*--------------------------------------------------------------------------*/
 
-var PeriodicalExecuter = Class.create();
-PeriodicalExecuter.prototype = {
+var PeriodicalExecuter = Class.create({
   initialize: function(callback, frequency) {
     this.callback = callback;
     this.frequency = frequency;
@@ -106,40 +294,87 @@
   },
 
   registerCallback: function() {
-    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  execute: function() {
+    this.callback(this);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
   },
 
   onTimerEvent: function() {
     if (!this.currentlyExecuting) {
       try {
         this.currentlyExecuting = true;
-        this.callback();
+        this.execute();
       } finally {
         this.currentlyExecuting = false;
       }
     }
   }
-}
+});
+Object.extend(String, {
+  interpret: function(value) {
+    return value == null ? '' : String(value);
+  },
+  specialChar: {
+    '\b': '\\b',
+    '\t': '\\t',
+    '\n': '\\n',
+    '\f': '\\f',
+    '\r': '\\r',
+    '\\': '\\\\'
+  }
+});
 
-/*--------------------------------------------------------------------------*/
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += String.interpret(replacement(match));
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
 
-function $() {
-  var elements = new Array();
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = count === undefined ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
 
-  for (var i = 0; i < arguments.length; i++) {
-    var element = arguments[i];
-    if (typeof element == 'string')
-      element = document.getElementById(element);
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return String(this);
+  },
 
-    if (arguments.length == 1)
-      return element;
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = truncation === undefined ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : String(this);
+  },
 
-    elements.push(element);
-  }
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
 
-  return elements;
-}
-Object.extend(String.prototype, {
   stripTags: function() {
     return this.replace(/<\/?[^>]+>/gi, '');
   },
@@ -157,28 +392,40 @@
   },
 
   evalScripts: function() {
-    return this.extractScripts().map(eval);
+    return this.extractScripts().map(function(script) { return eval(script) });
   },
 
   escapeHTML: function() {
-    var div = document.createElement('div');
-    var text = document.createTextNode(this);
-    div.appendChild(text);
-    return div.innerHTML;
+    var self = arguments.callee;
+    self.text.data = this;
+    return self.div.innerHTML;
   },
 
   unescapeHTML: function() {
-    var div = document.createElement('div');
+    var div = new Element('div');
     div.innerHTML = this.stripTags();
-    return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
-  },
-
-  toQueryParams: function() {
-    var pairs = this.match(/^\??(.*)$/)[1].split('&');
-    return pairs.inject({}, function(params, pairString) {
-      var pair = pairString.split('=');
-      params[pair[0]] = pair[1];
-      return params;
+    return div.childNodes[0] ? (div.childNodes.length > 1 ?
+      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+      div.childNodes[0].nodeValue) : '';
+  },
+
+  toQueryParams: function(separator) {
+    var match = this.strip().match(/([^?#]*)(#.*)?$/);
+    if (!match) return { };
+
+    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
+      if ((pair = pair.split('='))[0]) {
+        var key = decodeURIComponent(pair.shift());
+        var value = pair.length > 1 ? pair.join('=') : pair[0];
+        if (value != undefined) value = decodeURIComponent(value);
+
+        if (key in hash) {
+          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+          hash[key].push(value);
+        }
+        else hash[key] = value;
+      }
+      return hash;
     });
   },
 
@@ -186,67 +433,201 @@
     return this.split('');
   },
 
+  succ: function() {
+    return this.slice(0, this.length - 1) +
+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+  },
+
+  times: function(count) {
+    return count < 1 ? '' : new Array(count + 1).join(this);
+  },
+
   camelize: function() {
-    var oStringList = this.split('-');
-    if (oStringList.length == 1) return oStringList[0];
+    var parts = this.split('-'), len = parts.length;
+    if (len == 1) return parts[0];
 
-    var camelizedString = this.indexOf('-') == 0
-      ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
-      : oStringList[0];
+    var camelized = this.charAt(0) == '-'
+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+      : parts[0];
 
-    for (var i = 1, len = oStringList.length; i < len; i++) {
-      var s = oStringList[i];
-      camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
-    }
+    for (var i = 1; i < len; i++)
+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
 
-    return camelizedString;
+    return camelized;
   },
 
-  inspect: function() {
-    return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'";
+  capitalize: function() {
+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+  },
+
+  underscore: function() {
+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+  },
+
+  dasherize: function() {
+    return this.gsub(/_/,'-');
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+      var character = String.specialChar[match[0]];
+      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+    });
+    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+  },
+
+  toJSON: function() {
+    return this.inspect(true);
+  },
+
+  unfilterJSON: function(filter) {
+    return this.sub(filter || Prototype.JSONFilter, '#{1}');
+  },
+
+  isJSON: function() {
+    var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+  },
+
+  evalJSON: function(sanitize) {
+    var json = this.unfilterJSON();
+    try {
+      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+    } catch (e) { }
+    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+  },
+
+  include: function(pattern) {
+    return this.indexOf(pattern) > -1;
+  },
+
+  startsWith: function(pattern) {
+    return this.indexOf(pattern) === 0;
+  },
+
+  endsWith: function(pattern) {
+    var d = this.length - pattern.length;
+    return d >= 0 && this.lastIndexOf(pattern) === d;
+  },
+
+  empty: function() {
+    return this == '';
+  },
+
+  blank: function() {
+    return /^\s*$/.test(this);
+  },
+
+  interpolate: function(object, pattern) {
+    return new Template(this, pattern).evaluate(object);
+  }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+  escapeHTML: function() {
+    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+  },
+  unescapeHTML: function() {
+    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
   }
 });
 
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (Object.isFunction(replacement)) return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+};
+
 String.prototype.parseQuery = String.prototype.toQueryParams;
 
-var $break    = new Object();
-var $continue = new Object();
+Object.extend(String.prototype.escapeHTML, {
+  div:  document.createElement('div'),
+  text: document.createTextNode('')
+});
+
+with (String.prototype.escapeHTML) div.appendChild(text);
+
+var Template = Class.create({
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    if (Object.isFunction(object.toTemplateReplacements))
+      object = object.toTemplateReplacements();
+
+    return this.template.gsub(this.pattern, function(match) {
+      if (object == null) return '';
+
+      var before = match[1] || '';
+      if (before == '\\') return match[2];
+
+      var ctx = object, expr = match[3];
+      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/, match = pattern.exec(expr);
+      if (match == null) return before;
+
+      while (match != null) {
+        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
+        ctx = ctx[comp];
+        if (null == ctx || '' == match[3]) break;
+        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+        match = pattern.exec(expr);
+      }
+
+      return before + String.interpret(ctx);
+    }.bind(this));
+  }
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+
+var $break = { };
 
 var Enumerable = {
-  each: function(iterator) {
+  each: function(iterator, context) {
     var index = 0;
+    iterator = iterator.bind(context);
     try {
       this._each(function(value) {
-        try {
-          iterator(value, index++);
-        } catch (e) {
-          if (e != $continue) throw e;
-        }
+        iterator(value, index++);
       });
     } catch (e) {
       if (e != $break) throw e;
     }
+    return this;
+  },
+
+  eachSlice: function(number, iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var index = -number, slices = [], array = this.toArray();
+    while ((index += number) < array.length)
+      slices.push(array.slice(index, index+number));
+    return slices.collect(iterator, context);
   },
 
-  all: function(iterator) {
+  all: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
     var result = true;
     this.each(function(value, index) {
-      result = result && !!(iterator || Prototype.K)(value, index);
+      result = result && !!iterator(value, index);
       if (!result) throw $break;
     });
     return result;
   },
 
-  any: function(iterator) {
-    var result = true;
+  any: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var result = false;
     this.each(function(value, index) {
-      if (result = !!(iterator || Prototype.K)(value, index))
+      if (result = !!iterator(value, index))
         throw $break;
     });
     return result;
   },
 
-  collect: function(iterator) {
+  collect: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
     var results = [];
     this.each(function(value, index) {
       results.push(iterator(value, index));
@@ -254,7 +635,8 @@
     return results;
   },
 
-  detect: function (iterator) {
+  detect: function(iterator, context) {
+    iterator = iterator.bind(context);
     var result;
     this.each(function(value, index) {
       if (iterator(value, index)) {
@@ -265,7 +647,8 @@
     return result;
   },
 
-  findAll: function(iterator) {
+  findAll: function(iterator, context) {
+    iterator = iterator.bind(context);
     var results = [];
     this.each(function(value, index) {
       if (iterator(value, index))
@@ -274,17 +657,24 @@
     return results;
   },
 
-  grep: function(pattern, iterator) {
+  grep: function(filter, iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
     var results = [];
+
+    if (Object.isString(filter))
+      filter = new RegExp(filter);
+
     this.each(function(value, index) {
-      var stringValue = value.toString();
-      if (stringValue.match(pattern))
-        results.push((iterator || Prototype.K)(value, index));
-    })
+      if (filter.match(value))
+        results.push(iterator(value, index));
+    });
     return results;
   },
 
   include: function(object) {
+    if (Object.isFunction(this.indexOf))
+      if (this.indexOf(object) != -1) return true;
+
     var found = false;
     this.each(function(value) {
       if (value == object) {
@@ -295,7 +685,16 @@
     return found;
   },
 
-  inject: function(memo, iterator) {
+  inGroupsOf: function(number, fillWith) {
+    fillWith = fillWith === undefined ? null : fillWith;
+    return this.eachSlice(number, function(slice) {
+      while(slice.length < number) slice.push(fillWith);
+      return slice;
+    });
+  },
+
+  inject: function(memo, iterator, context) {
+    iterator = iterator.bind(context);
     this.each(function(value, index) {
       memo = iterator(memo, value, index);
     });
@@ -304,35 +703,38 @@
 
   invoke: function(method) {
     var args = $A(arguments).slice(1);
-    return this.collect(function(value) {
+    return this.map(function(value) {
       return value[method].apply(value, args);
     });
   },
 
-  max: function(iterator) {
+  max: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
     var result;
     this.each(function(value, index) {
-      value = (iterator || Prototype.K)(value, index);
-      if (value >= (result || value))
+      value = iterator(value, index);
+      if (result == undefined || value >= result)
         result = value;
     });
     return result;
   },
 
-  min: function(iterator) {
+  min: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
     var result;
     this.each(function(value, index) {
-      value = (iterator || Prototype.K)(value, index);
-      if (value <= (result || value))
+      value = iterator(value, index);
+      if (result == undefined || value < result)
         result = value;
     });
     return result;
   },
 
-  partition: function(iterator) {
+  partition: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
     var trues = [], falses = [];
     this.each(function(value, index) {
-      ((iterator || Prototype.K)(value, index) ?
+      (iterator(value, index) ?
         trues : falses).push(value);
     });
     return [trues, falses];
@@ -340,13 +742,14 @@
 
   pluck: function(property) {
     var results = [];
-    this.each(function(value, index) {
+    this.each(function(value) {
       results.push(value[property]);
     });
     return results;
   },
 
-  reject: function(iterator) {
+  reject: function(iterator, context) {
+    iterator = iterator.bind(context);
     var results = [];
     this.each(function(value, index) {
       if (!iterator(value, index))
@@ -355,8 +758,9 @@
     return results;
   },
 
-  sortBy: function(iterator) {
-    return this.collect(function(value, index) {
+  sortBy: function(iterator, context) {
+    iterator = iterator.bind(context);
+    return this.map(function(value, index) {
       return {value: value, criteria: iterator(value, index)};
     }).sort(function(left, right) {
       var a = left.criteria, b = right.criteria;
@@ -365,52 +769,67 @@
   },
 
   toArray: function() {
-    return this.collect(Prototype.K);
+    return this.map();
   },
 
   zip: function() {
     var iterator = Prototype.K, args = $A(arguments);
-    if (typeof args.last() == 'function')
+    if (Object.isFunction(args.last()))
       iterator = args.pop();
 
     var collections = [this].concat(args).map($A);
     return this.map(function(value, index) {
-      iterator(value = collections.pluck(index));
-      return value;
+      return iterator(collections.pluck(index));
     });
   },
 
+  size: function() {
+    return this.toArray().length;
+  },
+
   inspect: function() {
     return '#<Enumerable:' + this.toArray().inspect() + '>';
   }
-}
+};
 
 Object.extend(Enumerable, {
   map:     Enumerable.collect,
   find:    Enumerable.detect,
   select:  Enumerable.findAll,
+  filter:  Enumerable.findAll,
   member:  Enumerable.include,
-  entries: Enumerable.toArray
+  entries: Enumerable.toArray,
+  every:   Enumerable.all,
+  some:    Enumerable.any
 });
-var $A = Array.from = function(iterable) {
+function $A(iterable) {
   if (!iterable) return [];
-  if (iterable.toArray) {
-    return iterable.toArray();
-  } else {
-    var results = [];
-    for (var i = 0; i < iterable.length; i++)
-      results.push(iterable[i]);
+  if (iterable.toArray) return iterable.toArray();
+  var length = iterable.length, results = new Array(length);
+  while (length--) results[length] = iterable[length];
+  return results;
+}
+
+if (Prototype.Browser.WebKit) {
+  function $A(iterable) {
+    if (!iterable) return [];
+    if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
+        iterable.toArray) return iterable.toArray();
+    var length = iterable.length, results = new Array(length);
+    while (length--) results[length] = iterable[length];
     return results;
   }
 }
 
+Array.from = $A;
+
 Object.extend(Array.prototype, Enumerable);
 
-Array.prototype._reverse = Array.prototype.reverse;
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
 
 Object.extend(Array.prototype, {
   _each: function(iterator) {
-    for (var i = 0; i < this.length; i++)
+    for (var i = 0, length = this.length; i < length; i++)
       iterator(this[i]);
   },
 
@@ -429,13 +848,13 @@
 
   compact: function() {
     return this.select(function(value) {
-      return value != undefined || value != null;
+      return value != null;
     });
   },
 
   flatten: function() {
     return this.inject([], function(array, value) {
-      return array.concat(value.constructor == Array ?
+      return array.concat(Object.isArray(value) ?
         value.flatten() : [value]);
     });
   },
@@ -447,116 +866,278 @@
     });
   },
 
-  indexOf: function(object) {
-    for (var i = 0; i < this.length; i++)
-      if (this[i] == object) return i;
-    return -1;
-  },
-
   reverse: function(inline) {
     return (inline !== false ? this : this.toArray())._reverse();
   },
 
-  shift: function() {
-    var result = this[0];
-    for (var i = 0; i < this.length - 1; i++)
-      this[i] = this[i + 1];
-    this.length--;
-    return result;
+  reduce: function() {
+    return this.length > 1 ? this : this[0];
   },
 
-  inspect: function() {
-    return '[' + this.map(Object.inspect).join(', ') + ']';
-  }
-});
-var Hash = {
-  _each: function(iterator) {
-    for (key in this) {
-      var value = this[key];
-      if (typeof value == 'function') continue;
-
-      var pair = [key, value];
-      pair.key = key;
-      pair.value = value;
-      iterator(pair);
-    }
+  uniq: function(sorted) {
+    return this.inject([], function(array, value, index) {
+      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+        array.push(value);
+      return array;
+    });
   },
 
-  keys: function() {
-    return this.pluck('key');
+  intersect: function(array) {
+    return this.uniq().findAll(function(item) {
+      return array.detect(function(value) { return item === value });
+    });
   },
 
-  values: function() {
-    return this.pluck('value');
+  clone: function() {
+    return [].concat(this);
   },
 
-  merge: function(hash) {
-    return $H(hash).inject($H(this), function(mergedHash, pair) {
-      mergedHash[pair.key] = pair.value;
-      return mergedHash;
-    });
+  size: function() {
+    return this.length;
   },
 
-  toQueryString: function() {
-    return this.map(function(pair) {
-      return pair.map(encodeURIComponent).join('=');
-    }).join('&');
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
   },
 
-  inspect: function() {
-    return '#<Hash:{' + this.map(function(pair) {
-      return pair.map(Object.inspect).join(': ');
-    }).join(', ') + '}>';
+  toJSON: function() {
+    var results = [];
+    this.each(function(object) {
+      var value = Object.toJSON(object);
+      if (value !== undefined) results.push(value);
+    });
+    return '[' + results.join(', ') + ']';
   }
+});
+
+// use native browser JS 1.6 implementation if available
+if (Object.isFunction(Array.prototype.forEach))
+  Array.prototype._each = Array.prototype.forEach;
+
+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+  i || (i = 0);
+  var length = this.length;
+  if (i < 0) i = length + i;
+  for (; i < length; i++)
+    if (this[i] === item) return i;
+  return -1;
+};
+
+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
+  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+  var n = this.slice(0, i).reverse().indexOf(item);
+  return (n < 0) ? n : i - n - 1;
+};
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+  if (!Object.isString(string)) return [];
+  string = string.strip();
+  return string ? string.split(/\s+/) : [];
 }
 
-function $H(object) {
-  var hash = Object.extend({}, object || {});
-  Object.extend(hash, Enumerable);
-  Object.extend(hash, Hash);
-  return hash;
+if (Prototype.Browser.Opera){
+  Array.prototype.concat = function() {
+    var array = [];
+    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      if (Object.isArray(arguments[i])) {
+        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+          array.push(arguments[i][j]);
+      } else {
+        array.push(arguments[i]);
+      }
+    }
+    return array;
+  };
 }
-ObjectRange = Class.create();
-Object.extend(ObjectRange.prototype, Enumerable);
-Object.extend(ObjectRange.prototype, {
-  initialize: function(start, end, exclusive) {
-    this.start = start;
-    this.end = end;
-    this.exclusive = exclusive;
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    return this.toPaddedString(2, 16);
   },
 
-  _each: function(iterator) {
-    var value = this.start;
-    do {
-      iterator(value);
-      value = value.succ();
-    } while (this.include(value));
+  succ: function() {
+    return this + 1;
   },
 
-  include: function(value) {
-    if (value < this.start)
-      return false;
-    if (this.exclusive)
-      return value < this.end;
-    return value <= this.end;
-  }
-});
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  },
+
+  toPaddedString: function(length, radix) {
+    var string = this.toString(radix || 10);
+    return '0'.times(length - string.length) + string;
+  },
+
+  toJSON: function() {
+    return isFinite(this) ? this.toString() : 'null';
+  }
+});
+
+$w('abs round ceil floor').each(function(method){
+  Number.prototype[method] = Math[method].methodize();
+});
+function $H(object) {
+  return new Hash(object);
+};
+
+var Hash = Class.create(Enumerable, (function() {
+  if (function() {
+    var i = 0, Test = function(value) { this.key = value };
+    Test.prototype.key = 'foo';
+    for (var property in new Test('bar')) i++;
+    return i > 1;
+  }()) {
+    function each(iterator) {
+      var cache = [];
+      for (var key in this._object) {
+        var value = this._object[key];
+        if (cache.include(key)) continue;
+        cache.push(key);
+        var pair = [key, value];
+        pair.key = key;
+        pair.value = value;
+        iterator(pair);
+      }
+    }
+  } else {
+    function each(iterator) {
+      for (var key in this._object) {
+        var value = this._object[key], pair = [key, value];
+        pair.key = key;
+        pair.value = value;
+        iterator(pair);
+      }
+    }
+  }
+
+  function toQueryPair(key, value) {
+    if (Object.isUndefined(value)) return key;
+    return key + '=' + encodeURIComponent(String.interpret(value));
+  }
+
+  return {
+    initialize: function(object) {
+      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+    },
+
+    _each: each,
+
+    set: function(key, value) {
+      return this._object[key] = value;
+    },
+
+    get: function(key) {
+      return this._object[key];
+    },
+
+    unset: function(key) {
+      var value = this._object[key];
+      delete this._object[key];
+      return value;
+    },
+
+    toObject: function() {
+      return Object.clone(this._object);
+    },
+
+    keys: function() {
+      return this.pluck('key');
+    },
+
+    values: function() {
+      return this.pluck('value');
+    },
+
+    index: function(value) {
+      var match = this.detect(function(pair) {
+        return pair.value === value;
+      });
+      return match && match.key;
+    },
+
+    merge: function(object) {
+      return this.clone().update(object);
+    },
+
+    update: function(object) {
+      return new Hash(object).inject(this, function(result, pair) {
+        result.set(pair.key, pair.value);
+        return result;
+      });
+    },
+
+    toQueryString: function() {
+      return this.map(function(pair) {
+        var key = encodeURIComponent(pair.key), values = pair.value;
+
+        if (values && typeof values == 'object') {
+          if (Object.isArray(values))
+            return values.map(toQueryPair.curry(key)).join('&');
+        }
+        return toQueryPair(key, values);
+      }).join('&');
+    },
+
+    inspect: function() {
+      return '#<Hash:{' + this.map(function(pair) {
+        return pair.map(Object.inspect).join(': ');
+      }).join(', ') + '}>';
+    },
+
+    toJSON: function() {
+      return Object.toJSON(this.toObject());
+    },
+
+    clone: function() {
+      return new Hash(this);
+    }
+  }
+})());
+
+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
+Hash.from = $H;
+var ObjectRange = Class.create(Enumerable, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
 
 var $R = function(start, end, exclusive) {
   return new ObjectRange(start, end, exclusive);
-}
+};
 
 var Ajax = {
   getTransport: function() {
     return Try.these(
+      function() {return new XMLHttpRequest()},
       function() {return new ActiveXObject('Msxml2.XMLHTTP')},
-      function() {return new ActiveXObject('Microsoft.XMLHTTP')},
-      function() {return new XMLHttpRequest()}
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
     ) || false;
   },
 
   activeRequestCount: 0
-}
+};
 
 Ajax.Responders = {
   responders: [],
@@ -565,21 +1146,21 @@
     this.responders._each(iterator);
   },
 
-  register: function(responderToAdd) {
-    if (!this.include(responderToAdd))
-      this.responders.push(responderToAdd);
+  register: function(responder) {
+    if (!this.include(responder))
+      this.responders.push(responder);
   },
 
-  unregister: function(responderToRemove) {
-    this.responders = this.responders.without(responderToRemove);
+  unregister: function(responder) {
+    this.responders = this.responders.without(responder);
   },
 
   dispatch: function(callback, request, transport, json) {
     this.each(function(responder) {
-      if (responder[callback] && typeof responder[callback] == 'function') {
+      if (Object.isFunction(responder[callback])) {
         try {
           responder[callback].apply(responder, [request, transport, json]);
-        } catch (e) {}
+        } catch (e) { }
       }
     });
   }
@@ -588,159 +1169,189 @@
 Object.extend(Ajax.Responders, Enumerable);
 
 Ajax.Responders.register({
-  onCreate: function() {
-    Ajax.activeRequestCount++;
-  },
-
-  onComplete: function() {
-    Ajax.activeRequestCount--;
-  }
+  onCreate:   function() { Ajax.activeRequestCount++ },
+  onComplete: function() { Ajax.activeRequestCount-- }
 });
 
-Ajax.Base = function() {};
-Ajax.Base.prototype = {
-  setOptions: function(options) {
+Ajax.Base = Class.create({
+  initialize: function(options) {
     this.options = {
       method:       'post',
       asynchronous: true,
-      parameters:   ''
-    }
-    Object.extend(this.options, options || {});
-  },
-
-  responseIsSuccess: function() {
-    return this.transport.status == undefined
-        || this.transport.status == 0
-        || (this.transport.status >= 200 && this.transport.status < 300);
-  },
-
-  responseIsFailure: function() {
-    return !this.responseIsSuccess();
+      contentType:  'application/x-www-form-urlencoded',
+      encoding:     'UTF-8',
+      parameters:   '',
+      evalJSON:     true,
+      evalJS:       true
+    };
+    Object.extend(this.options, options || { });
+
+    this.options.method = this.options.method.toLowerCase();
+    if (Object.isString(this.options.parameters))
+      this.options.parameters = this.options.parameters.toQueryParams();
   }
-}
+});
 
-Ajax.Request = Class.create();
-Ajax.Request.Events =
-  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+Ajax.Request = Class.create(Ajax.Base, {
+  _complete: false,
 
-Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
-  initialize: function(url, options) {
+  initialize: function($super, url, options) {
+    $super(options);
     this.transport = Ajax.getTransport();
-    this.setOptions(options);
     this.request(url);
   },
 
   request: function(url) {
-    var parameters = this.options.parameters || '';
-    if (parameters.length > 0) parameters += '&_=';
+    this.url = url;
+    this.method = this.options.method;
+    var params = Object.clone(this.options.parameters);
 
-    try {
-      this.url = url;
-      if (this.options.method == 'get' && parameters.length > 0)
-        this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;
+    if (!['get', 'post'].include(this.method)) {
+      // simulate other verbs over post
+      params['_method'] = this.method;
+      this.method = 'post';
+    }
+
+    this.parameters = params;
+
+    if (params = Object.toQueryString(params)) {
+      // when GET, append parameters to URL
+      if (this.method == 'get')
+        this.url += (this.url.include('?') ? '&' : '?') + params;
+      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+        params += '&_=';
+    }
 
-      Ajax.Responders.dispatch('onCreate', this, this.transport);
+    try {
+      var response = new Ajax.Response(this);
+      if (this.options.onCreate) this.options.onCreate(response);
+      Ajax.Responders.dispatch('onCreate', this, response);
 
-      this.transport.open(this.options.method, this.url,
+      this.transport.open(this.method.toUpperCase(), this.url,
         this.options.asynchronous);
 
-      if (this.options.asynchronous) {
-        setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
-      }
-      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
 
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
       this.setRequestHeaders();
 
-      var body = this.options.postBody ? this.options.postBody : parameters;
-      this.transport.send(this.options.method == 'post' ? body : null);
+      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+      this.transport.send(this.body);
 
-    } catch (e) {
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous && this.transport.overrideMimeType)
+        this.onStateChange();
+
+    }
+    catch (e) {
       this.dispatchException(e);
     }
   },
 
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState > 1 && !((readyState == 4) && this._complete))
+      this.respondToReadyState(this.transport.readyState);
+  },
+
   setRequestHeaders: function() {
-    var requestHeaders =
-      ['X-Requested-With', 'XMLHttpRequest',
-       'X-Prototype-Version', Prototype.Version];
-
-    if (this.options.method == 'post') {
-      var hasContentType = 0;
-      if (this.options.requestHeaders)
-          for (var i = 0; i < this.options.requestHeaders.length; i += 2) 
-              if (this.options.requestHeaders[i] == 'Content-Type')
-                  hasContentType = 1;
-      if (hasContentType == 0)
-          requestHeaders.push('Content-Type', 'application/x-www-form-urlencoded');
-
-      /* Force "Connection: close" for Mozilla browsers to work around
-       * a bug where XMLHttpReqeuest sends an incorrect Content-length
-       * header. See Mozilla Bugzilla #246651.
+    var headers = {
+      'X-Requested-With': 'XMLHttpRequest',
+      'X-Prototype-Version': Prototype.Version,
+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+    };
+
+    if (this.method == 'post') {
+          headers['Content-Type'] = this.options.contentType +
+            (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+      /* Force "Connection: close" for older Mozilla browsers to work
+       * around a bug where XMLHttpRequest sends an incorrect
+       * Content-length header. See Mozilla Bugzilla #246651.
        */
-      if (this.transport.overrideMimeType)
-        requestHeaders.push('Connection', 'close');
+      if (this.transport.overrideMimeType &&
+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) {
+            headers['Connection'] = 'close';
+          }
     }
 
-    if (this.options.requestHeaders)
-      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
-
-    for (var i = 0; i < requestHeaders.length; i += 2)
-      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
-  },
+    // user-defined headers
+    if (typeof this.options.requestHeaders == 'object') {
+      var extras = this.options.requestHeaders;
+
+      if (Object.isFunction(extras.push))
+        for (var i = 0, length = extras.length; i < length; i += 2) {
+          headers[extras[i]] = extras[i+1];
+        }
+      else
+        $H(extras).each(function(pair) {
+            headers[pair.key] = pair.value 
+        });
+    }
 
-  onStateChange: function() {
-    var readyState = this.transport.readyState;
-    if (readyState != 1)
-      this.respondToReadyState(this.transport.readyState);
-  },
+    for (var name in headers) {
+        if ( typeof headers[name] == "string" ) 
+            this.transport.setRequestHeader(name, headers[name]);
+    }
 
-  header: function(name) {
-    try {
-      return this.transport.getResponseHeader(name);
-    } catch (e) {}
   },
 
-  evalJSON: function() {
-    try {
-      return eval(this.header('X-JSON'));
-    } catch (e) {}
+  success: function() {
+    var status = this.getStatus();
+    return !status || (status >= 200 && status < 300);
   },
 
-  evalResponse: function() {
+  getStatus: function() {
     try {
-      return eval(this.transport.responseText);
-    } catch (e) {
-      this.dispatchException(e);
-    }
+      return this.transport.status || 0;
+    } catch (e) { return 0 }
   },
 
   respondToReadyState: function(readyState) {
-    var event = Ajax.Request.Events[readyState];
-    var transport = this.transport, json = this.evalJSON();
+    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
 
-    if (event == 'Complete') {
+    if (state == 'Complete') {
       try {
-        (this.options['on' + this.transport.status]
-         || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
-         || Prototype.emptyFunction)(transport, json);
+        this._complete = true;
+        (this.options['on' + response.status]
+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(response, response.headerJSON);
       } catch (e) {
         this.dispatchException(e);
       }
 
-      if ((this.header('Content-type') || '').match(/^text\/javascript/i))
+      var contentType = response.getHeader('Content-type');
+      if (this.options.evalJS == 'force'
+          || (this.options.evalJS && contentType
+          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
         this.evalResponse();
     }
 
     try {
-      (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
-      Ajax.Responders.dispatch('on' + event, this, transport, json);
+      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
     } catch (e) {
       this.dispatchException(e);
     }
 
-    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
-    if (event == 'Complete')
+    if (state == 'Complete') {
+      // avoid memory leak in MSIE: clean up
       this.transport.onreadystatechange = Prototype.emptyFunction;
+    }
+  },
+
+  getHeader: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) { return null }
+  },
+
+  evalResponse: function() {
+    try {
+      return eval((this.transport.responseText || '').unfilterJSON());
+    } catch (e) {
+      this.dispatchException(e);
+    }
   },
 
   dispatchException: function(exception) {
@@ -749,61 +1360,129 @@
   }
 });
 
-Ajax.Updater = Class.create();
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Response = Class.create({
+  initialize: function(request){
+    this.request = request;
+    var transport  = this.transport  = request.transport,
+        readyState = this.readyState = transport.readyState;
 
-Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
-  initialize: function(container, url, options) {
-    this.containers = {
-      success: container.success ? $(container.success) : $(container),
-      failure: container.failure ? $(container.failure) :
-        (container.success ? null : $(container))
+    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+      this.status       = this.getStatus();
+      this.statusText   = this.getStatusText();
+      this.responseText = String.interpret(transport.responseText);
+      this.headerJSON   = this._getHeaderJSON();
     }
 
-    this.transport = Ajax.getTransport();
-    this.setOptions(options);
+    if(readyState == 4) {
+      var xml = transport.responseXML;
+      this.responseXML  = xml === undefined ? null : xml;
+      this.responseJSON = this._getResponseJSON();
+    }
+  },
+
+  status:      0,
+  statusText: '',
 
-    var onComplete = this.options.onComplete || Prototype.emptyFunction;
-    this.options.onComplete = (function(transport, object) {
-      this.updateContent();
-      onComplete(transport, object);
+  getStatus: Ajax.Request.prototype.getStatus,
+
+  getStatusText: function() {
+    try {
+      return this.transport.statusText || '';
+    } catch (e) { return '' }
+  },
+
+  getHeader: Ajax.Request.prototype.getHeader,
+
+  getAllHeaders: function() {
+    try {
+      return this.getAllResponseHeaders();
+    } catch (e) { return null }
+  },
+
+  getResponseHeader: function(name) {
+    return this.transport.getResponseHeader(name);
+  },
+
+  getAllResponseHeaders: function() {
+    return this.transport.getAllResponseHeaders();
+  },
+
+  _getHeaderJSON: function() {
+    var json = this.getHeader('X-JSON');
+    if (!json) return null;
+    json = decodeURIComponent(escape(json));
+    try {
+      return json.evalJSON(this.request.options.sanitizeJSON);
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  },
+
+  _getResponseJSON: function() {
+    var options = this.request.options;
+    if (!options.evalJSON || (options.evalJSON != 'force' &&
+      !(this.getHeader('Content-type') || '').include('application/json')))
+        return null;
+    try {
+      return this.transport.responseText.evalJSON(options.sanitizeJSON);
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+  initialize: function($super, container, url, options) {
+    this.container = {
+      success: (container.success || container),
+      failure: (container.failure || (container.success ? null : container))
+    };
+
+    options = options || { };
+    var onComplete = options.onComplete;
+    options.onComplete = (function(response, param) {
+      this.updateContent(response.responseText);
+      if (Object.isFunction(onComplete)) onComplete(response, param);
     }).bind(this);
 
-    this.request(url);
+    $super(url, options);
   },
 
-  updateContent: function() {
-    var receiver = this.responseIsSuccess() ?
-      this.containers.success : this.containers.failure;
-    var response = this.transport.responseText;
-
-    if (!this.options.evalScripts)
-      response = response.stripScripts();
-
-    if (receiver) {
-      if (this.options.insertion) {
-        new this.options.insertion(receiver, response);
-      } else {
-        Element.update(receiver, response);
+  updateContent: function(responseText) {
+    var receiver = this.container[this.success() ? 'success' : 'failure'],
+        options = this.options;
+
+    if (!options.evalScripts) responseText = responseText.stripScripts();
+
+    if (receiver = $(receiver)) {
+      if (options.insertion) {
+        if (Object.isString(options.insertion)) {
+          var insertion = { }; insertion[options.insertion] = responseText;
+          receiver.insert(insertion);
+        }
+        else options.insertion(receiver, responseText);
       }
+      else receiver.update(responseText);
     }
 
-    if (this.responseIsSuccess()) {
-      if (this.onComplete)
-        setTimeout(this.onComplete.bind(this), 10);
+    if (this.success()) {
+      if (this.onComplete) this.onComplete.bind(this).defer();
     }
   }
 });
 
-Ajax.PeriodicalUpdater = Class.create();
-Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
-  initialize: function(container, url, options) {
-    this.setOptions(options);
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+  initialize: function($super, container, url, options) {
+    $super(options);
     this.onComplete = this.options.onComplete;
 
     this.frequency = (this.options.frequency || 2);
     this.decay = (this.options.decay || 1);
 
-    this.updater = {};
+    this.updater = { };
     this.container = container;
     this.url = url;
 
@@ -816,574 +1495,2142 @@
   },
 
   stop: function() {
-    this.updater.onComplete = undefined;
+    this.updater.options.onComplete = undefined;
     clearTimeout(this.timer);
     (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
   },
 
-  updateComplete: function(request) {
+  updateComplete: function(response) {
     if (this.options.decay) {
-      this.decay = (request.responseText == this.lastText ?
+      this.decay = (response.responseText == this.lastText ?
         this.decay * this.options.decay : 1);
 
-      this.lastText = request.responseText;
+      this.lastText = response.responseText;
     }
-    this.timer = setTimeout(this.onTimerEvent.bind(this),
-      this.decay * this.frequency * 1000);
+    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
   },
 
   onTimerEvent: function() {
     this.updater = new Ajax.Updater(this.container, this.url, this.options);
   }
 });
-document.getElementsByClassName = function(className, parentElement) {
-  var children = ($(parentElement) || document.body).getElementsByTagName('*');
-  return $A(children).inject([], function(elements, child) {
-    if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
-      elements.push(child);
+function $(element) {
+  if (arguments.length > 1) {
+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+      elements.push($(arguments[i]));
     return elements;
-  });
+  }
+  if (Object.isString(element))
+    element = document.getElementById(element);
+  return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+  document._getElementsByXPath = function(expression, parentElement) {
+    var results = [];
+    var query = document.evaluate(expression, $(parentElement) || document,
+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    for (var i = 0, length = query.snapshotLength; i < length; i++)
+      results.push(Element.extend(query.snapshotItem(i)));
+    return results;
+  };
 }
 
 /*--------------------------------------------------------------------------*/
 
-if (!window.Element) {
-  var Element = new Object();
+if (!window.Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+  // DOM level 2 ECMAScript Language Binding
+  Object.extend(Node, {
+    ELEMENT_NODE: 1,
+    ATTRIBUTE_NODE: 2,
+    TEXT_NODE: 3,
+    CDATA_SECTION_NODE: 4,
+    ENTITY_REFERENCE_NODE: 5,
+    ENTITY_NODE: 6,
+    PROCESSING_INSTRUCTION_NODE: 7,
+    COMMENT_NODE: 8,
+    DOCUMENT_NODE: 9,
+    DOCUMENT_TYPE_NODE: 10,
+    DOCUMENT_FRAGMENT_NODE: 11,
+    NOTATION_NODE: 12
+  });
 }
 
-Object.extend(Element, {
+(function() {
+  var element = this.Element;
+  this.Element = function(tagName, attributes) {
+    attributes = attributes || { };
+    tagName = tagName.toLowerCase();
+    var cache = Element.cache;
+    if (Prototype.Browser.IE && attributes.name) {
+      tagName = '<' + tagName + ' name="' + attributes.name + '">';
+      delete attributes.name;
+      return Element.writeAttribute(document.createElement(tagName), attributes);
+    }
+    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+  };
+  Object.extend(this.Element, element || { });
+}).call(window);
+
+Element.cache = { };
+
+Element.Methods = {
   visible: function(element) {
     return $(element).style.display != 'none';
   },
 
-  toggle: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var element = $(arguments[i]);
-      Element[Element.visible(element) ? 'hide' : 'show'](element);
-    }
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
   },
 
-  hide: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var element = $(arguments[i]);
-      element.style.display = 'none';
-    }
+  hide: function(element) {
+    $(element).style.display = 'none';
+    return element;
   },
 
-  show: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var element = $(arguments[i]);
-      element.style.display = '';
-    }
+  show: function(element) {
+    $(element).style.display = '';
+    return element;
   },
 
   remove: function(element) {
     element = $(element);
     element.parentNode.removeChild(element);
+    return element;
   },
 
-  update: function(element, html) {
-    $(element).innerHTML = html.stripScripts();
-    setTimeout(function() {html.evalScripts()}, 10);
+  update: function(element, content) {
+    element = $(element);
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+    content = Object.toHTML(content);
+    element.innerHTML = content.stripScripts();
+    content.evalScripts.bind(content).defer();
+    return element;
   },
 
-  getHeight: function(element) {
+  replace: function(element, content) {
     element = $(element);
-    return element.offsetHeight;
+    if (content && content.toElement) content = content.toElement();
+    else if (!Object.isElement(content)) {
+      content = Object.toHTML(content);
+      var range = element.ownerDocument.createRange();
+      range.selectNode(element);
+      content.evalScripts.bind(content).defer();
+      content = range.createContextualFragment(content.stripScripts());
+    }
+    element.parentNode.replaceChild(content, element);
+    return element;
   },
 
-  classNames: function(element) {
-    return new Element.ClassNames(element);
-  },
+  insert: function(element, insertions) {
+    element = $(element);
 
-  hasClassName: function(element, className) {
-    if (!(element = $(element))) return;
-    return Element.classNames(element).include(className);
+    if (Object.isString(insertions) || Object.isNumber(insertions) ||
+        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+          insertions = {bottom:insertions};
+
+    var content, t, range;
+
+    for (position in insertions) {
+      if ( position == 'extend' ) continue;
+      content  = insertions[position];
+      position = position.toLowerCase();
+      t = Element._insertionTranslations[position];
+
+      if (content && content.toElement) content = content.toElement();
+      if (Object.isElement(content)) {
+        t.insert(element, content);
+        continue;
+      }
+
+      content = Object.toHTML(content);
+
+      range = element.ownerDocument.createRange();
+      t.initializeRange(element, range);
+      t.insert(element, range.createContextualFragment(content.stripScripts()));
+
+      content.evalScripts.bind(content).defer();
+    }
+
+    return element;
   },
 
-  addClassName: function(element, className) {
-    if (!(element = $(element))) return;
-    return Element.classNames(element).add(className);
+  wrap: function(element, wrapper, attributes) {
+    element = $(element);
+    if (Object.isElement(wrapper))
+      $(wrapper).writeAttribute(attributes || { });
+    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+    else wrapper = new Element('div', wrapper);
+    if (element.parentNode)
+      element.parentNode.replaceChild(wrapper, element);
+    wrapper.appendChild(element);
+    return wrapper;
   },
 
-  removeClassName: function(element, className) {
-    if (!(element = $(element))) return;
-    return Element.classNames(element).remove(className);
+  inspect: function(element) {
+    element = $(element);
+    var result = '<' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '>';
   },
 
-  // removes whitespace-only text node children
-  cleanWhitespace: function(element) {
+  recursivelyCollect: function(element, property) {
     element = $(element);
-    for (var i = 0; i < element.childNodes.length; i++) {
-      var node = element.childNodes[i];
-      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
-        Element.remove(node);
-    }
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
   },
 
-  empty: function(element) {
-    return $(element).innerHTML.match(/^\s*$/);
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
   },
 
-  scrollTo: function(element) {
-    element = $(element);
-    var x = element.x ? element.x : element.offsetLeft,
-        y = element.y ? element.y : element.offsetTop;
-    window.scrollTo(x, y);
+  descendants: function(element) {
+    return $A($(element).getElementsByTagName('*')).each(Element.extend);
   },
 
-  getStyle: function(element, style) {
-    element = $(element);
-    var value = element.style[style.camelize()];
-    if (!value) {
-      if (document.defaultView && document.defaultView.getComputedStyle) {
-        var css = document.defaultView.getComputedStyle(element, null);
-        value = css ? css.getPropertyValue(style) : null;
-      } else if (element.currentStyle) {
-        value = element.currentStyle[style.camelize()];
-      }
-    }
+  firstDescendant: function(element) {
+    element = $(element).firstChild;
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    return $(element);
+  },
 
-    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
-      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+  immediateDescendants: function(element) {
+    if (!(element = $(element).firstChild)) return [];
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    if (element) return [element].concat($(element).nextSiblings());
+    return [];
+  },
 
-    return value == 'auto' ? null : value;
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
   },
 
-  setStyle: function(element, style) {
-    element = $(element);
-    for (name in style)
-      element.style[name.camelize()] = style[name];
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
   },
 
-  getDimensions: function(element) {
+  siblings: function(element) {
     element = $(element);
-    if (Element.getStyle(element, 'display') != 'none')
-      return {width: element.offsetWidth, height: element.offsetHeight};
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
 
-    // All *Width and *Height properties give 0 on elements with display none,
-    // so enable the element temporarily
-    var els = element.style;
-    var originalVisibility = els.visibility;
-    var originalPosition = els.position;
-    els.visibility = 'hidden';
-    els.position = 'absolute';
-    els.display = '';
-    var originalWidth = element.clientWidth;
-    var originalHeight = element.clientHeight;
-    els.display = 'none';
-    els.position = originalPosition;
-    els.visibility = originalVisibility;
-    return {width: originalWidth, height: originalHeight};
+  match: function(element, selector) {
+    if (Object.isString(selector))
+      selector = new Selector(selector);
+    return selector.match($(element));
   },
 
-  makePositioned: function(element) {
+  up: function(element, expression, index) {
     element = $(element);
-    var pos = Element.getStyle(element, 'position');
-    if (pos == 'static' || !pos) {
-      element._madePositioned = true;
-      element.style.position = 'relative';
-      // Opera returns the offset relative to the positioning context, when an
-      // element is position relative but top and left have not been defined
-      if (window.opera) {
-        element.style.top = 0;
-        element.style.left = 0;
-      }
-    }
+    if (arguments.length == 1) return $(element.parentNode);
+    var ancestors = element.ancestors();
+    return expression ? Selector.findElement(ancestors, expression, index) :
+      ancestors[index || 0];
   },
 
-  undoPositioned: function(element) {
+  down: function(element, expression, index) {
     element = $(element);
-    if (element._madePositioned) {
-      element._madePositioned = undefined;
-      element.style.position =
-        element.style.top =
-        element.style.left =
-        element.style.bottom =
-        element.style.right = '';
-    }
+    if (arguments.length == 1) return element.firstDescendant();
+    var descendants = element.descendants();
+    return expression ? Selector.findElement(descendants, expression, index) :
+      descendants[index || 0];
   },
 
-  makeClipping: function(element) {
+  previous: function(element, expression, index) {
     element = $(element);
-    if (element._overflow) return;
-    element._overflow = element.style.overflow;
-    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
-      element.style.overflow = 'hidden';
+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+    var previousSiblings = element.previousSiblings();
+    return expression ? Selector.findElement(previousSiblings, expression, index) :
+      previousSiblings[index || 0];
   },
 
-  undoClipping: function(element) {
+  next: function(element, expression, index) {
     element = $(element);
-    if (element._overflow) return;
-    element.style.overflow = element._overflow;
-    element._overflow = undefined;
+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+    var nextSiblings = element.nextSiblings();
+    return expression ? Selector.findElement(nextSiblings, expression, index) :
+      nextSiblings[index || 0];
+  },
+
+  select: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  adjacent: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element.parentNode, args).without(element);
+  },
+
+  identify: function(element) {
+    element = $(element);
+    var id = element.readAttribute('id'), self = arguments.callee;
+    if (id) return id;
+    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
+    element.writeAttribute('id', id);
+    return id;
+  },
+
+  readAttribute: function(element, name) {
+    element = $(element);
+    if (Prototype.Browser.IE) {
+      var t = Element._attributeTranslations.read;
+      if (t.values[name]) return t.values[name](element, name);
+      if (t.names[name]) name = t.names[name];
+      if (name.include(':')) {
+        return (!element.attributes || !element.attributes[name]) ? null :
+         element.attributes[name].value;
+      }
+    }
+    return element.getAttribute(name);
+  },
+
+  writeAttribute: function(element, name, value) {
+    element = $(element);
+    var attributes = { }, t = Element._attributeTranslations.write;
+
+    if (typeof name == 'object') attributes = name;
+    else attributes[name] = value === undefined ? true : value;
+
+    for (var attr in attributes) {
+      var name = t.names[attr] || attr, value = attributes[attr];
+      if (t.values[attr]) name = t.values[attr](element, value);
+      if (value === false || value === null)
+        element.removeAttribute(name);
+      else if (value === true)
+        element.setAttribute(name, name);
+      else element.setAttribute(name, value);
+    }
+    return element;
+  },
+
+  getHeight: function(element) {
+    return $(element).getDimensions().height;
+  },
+
+  getWidth: function(element) {
+    return $(element).getDimensions().width;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    var elementClassName = element.className;
+    return (elementClassName.length > 0 && (elementClassName == className ||
+      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    if (!element.hasClassName(className))
+      element.className += (element.className ? ' ' : '') + className;
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    element.className = element.className.replace(
+      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
+    return element;
+  },
+
+  toggleClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return element[element.hasClassName(className) ?
+      'removeClassName' : 'addClassName'](className);
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.blank();
+  },
+
+  descendantOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+
+    if (element.compareDocumentPosition)
+      return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+    if (element.sourceIndex && !Prototype.Browser.Opera) {
+      var e = element.sourceIndex, a = ancestor.sourceIndex,
+       nextAncestor = ancestor.nextSibling;
+      if (!nextAncestor) {
+        do { ancestor = ancestor.parentNode; }
+        while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
+      }
+      if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex);
+    }
+
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var pos = element.cumulativeOffset();
+    window.scrollTo(pos[0], pos[1]);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    style = style == 'float' ? 'cssFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value) {
+      var css = document.defaultView.getComputedStyle(element, null);
+      value = css ? css[style] : null;
+    }
+    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+    return value == 'auto' ? null : value;
+  },
+
+  getOpacity: function(element) {
+    return $(element).getStyle('opacity');
+  },
+
+  setStyle: function(element, styles) {
+    element = $(element);
+    var elementStyle = element.style, match;
+    if (Object.isString(styles)) {
+      element.style.cssText += ';' + styles;
+      return styles.include('opacity') ?
+        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
+    }
+    for (var property in styles)
+      if (property == 'opacity') element.setOpacity(styles[property]);
+      else
+        elementStyle[(property == 'float' || property == 'cssFloat') ?
+          (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') :
+            property] = styles[property];
+
+    return element;
+  },
+
+  setOpacity: function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    var display = $(element).getStyle('display');
+    if (display != 'none' && display != null) // Safari bug
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    var originalDisplay = els.display;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = 'block';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = originalDisplay;
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+    return element;
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return element;
+    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+    if (element._overflow !== 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return element;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        if (element.tagName == 'BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p == 'relative' || p == 'absolute') break;
+      }
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'absolute') return;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+    var offsets = element.positionedOffset();
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.width  = width + 'px';
+    element.style.height = height + 'px';
+    return element;
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'relative') return;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+    return element;
+  },
+
+  cumulativeScrollOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  getOffsetParent: function(element) {
+    if (element.offsetParent) return $(element.offsetParent);
+    if (element == document.body) return $(element);
+
+    while ((element = element.parentNode) && element != document.body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return $(element);
+
+    return $(document.body);
+  },
+
+  viewportOffset: function(forElement) {
+    var valueT = 0, valueL = 0;
+
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+
+      // Safari fix
+      if (element.offsetParent == document.body &&
+        Element.getStyle(element, 'position') == 'absolute') break;
+
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      if (!Prototype.Browser.Opera || element.tagName == 'BODY') {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
+      }
+    } while (element = element.parentNode);
+
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  clonePosition: function(element, source) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || { });
+
+    // find page position of source
+    source = $(source);
+    var p = source.viewportOffset();
+
+    // find coordinate system to use
+    element = $(element);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(element, 'position') == 'absolute') {
+      parent = element.getOffsetParent();
+      delta = parent.viewportOffset();
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
+    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
+    return element;
   }
+};
+
+Element.Methods.identify.counter = 1;
+
+Object.extend(Element.Methods, {
+  getElementsBySelector: Element.Methods.select,
+  childElements: Element.Methods.immediateDescendants
 });
 
-var Toggle = new Object();
-Toggle.display = Element.toggle;
+Element._attributeTranslations = {
+  write: {
+    names: {
+      className: 'class',
+      htmlFor:   'for'
+    },
+    values: { }
+  }
+};
+
+
+if (!document.createRange || Prototype.Browser.Opera) {
+  Element.Methods.insert = function(element, insertions) {
+    element = $(element);
+
+    if (Object.isString(insertions) || Object.isNumber(insertions) ||
+        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+          insertions = { bottom: insertions };
+
+    var t = Element._insertionTranslations, content, position, pos, tagName;
+
+    for (position in insertions) {
+      content  = insertions[position];
+      position = position.toLowerCase();
+      pos      = t[position];
+
+      if (content && content.toElement) content = content.toElement();
+      if (Object.isElement(content)) {
+        pos.insert(element, content);
+        continue;
+      }
+
+      content = Object.toHTML(content);
+      tagName = ((position == 'before' || position == 'after')
+        ? element.parentNode : element).tagName.toUpperCase();
+
+      if (t.tags[tagName]) {
+        var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+        if (position == 'top' || position == 'after') fragments.reverse();
+        fragments.each(pos.insert.curry(element));
+      }
+      else element.insertAdjacentHTML(pos.adjacency, content.stripScripts());
+
+      content.evalScripts.bind(content).defer();
+    }
+
+    return element;
+  };
+}
+
+if (Prototype.Browser.Opera) {
+  Element.Methods._getStyle = Element.Methods.getStyle;
+  Element.Methods.getStyle = function(element, style) {
+    switch(style) {
+      case 'left':
+      case 'top':
+      case 'right':
+      case 'bottom':
+        if (Element._getStyle(element, 'position') == 'static') return null;
+      default: return Element._getStyle(element, style);
+    }
+  };
+  Element.Methods._readAttribute = Element.Methods.readAttribute;
+  Element.Methods.readAttribute = function(element, attribute) {
+    if (attribute == 'title') return element.title;
+    return Element._readAttribute(element, attribute);
+  };
+}
+
+else if (Prototype.Browser.IE) {
+  $w('positionedOffset getOffsetParent viewportOffset').each(function(method) {
+    Element.Methods[method] = Element.Methods[method].wrap(
+      function(proceed, element) {
+        element = $(element);
+        var position = element.getStyle('position');
+        if (position != 'static') return proceed(element);
+        element.setStyle({ position: 'relative' });
+        var value = proceed(element);
+        element.setStyle({ position: position });
+        return value;
+      }
+    );
+  });
+
+  Element.Methods.getStyle = function(element, style) {
+    element = $(element);
+    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value && element.currentStyle) value = element.currentStyle[style];
+
+    if (style == 'opacity') {
+      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+        if (value[1]) return parseFloat(value[1]) / 100;
+      return 1.0;
+    }
+
+    if (value == 'auto') {
+      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+        return element['offset' + style.capitalize()] + 'px';
+      return null;
+    }
+    return value;
+  };
+
+  Element.Methods.setOpacity = function(element, value) {
+    function stripAlpha(filter){
+      return filter.replace(/alpha\([^\)]*\)/gi,'');
+    }
+    element = $(element);
+    var currentStyle = element.currentStyle;
+    if ((currentStyle && !currentStyle.hasLayout) ||
+      (!currentStyle && element.style.zoom == 'normal'))
+        element.style.zoom = 1;
+
+    var filter = element.getStyle('filter'), style = element.style;
+    if (value == 1 || value === '') {
+      (filter = stripAlpha(filter)) ?
+        style.filter = filter : style.removeAttribute('filter');
+      return element;
+    } else if (value < 0.00001) value = 0;
+    style.filter = stripAlpha(filter) +
+      'alpha(opacity=' + (value * 100) + ')';
+    return element;
+  };
+
+  Element._attributeTranslations = {
+    read: {
+      names: {
+        'class': 'className',
+        'for':   'htmlFor'
+      },
+      values: {
+        _getAttr: function(element, attribute) {
+          return element.getAttribute(attribute, 2);
+        },
+        _getAttrNode: function(element, attribute) {
+          var node = element.getAttributeNode(attribute);
+          return node ? node.value : "";
+        },
+        _getEv: function(element, attribute) {
+          var attribute = element.getAttribute(attribute);
+          return attribute ? attribute.toString().slice(23, -2) : null;
+        },
+        _flag: function(element, attribute) {
+          return $(element).hasAttribute(attribute) ? attribute : null;
+        },
+        style: function(element) {
+          return element.style.cssText.toLowerCase();
+        },
+        title: function(element) {
+          return element.title;
+        }
+      }
+    }
+  };
+
+  Element._attributeTranslations.write = {
+    names: Object.clone(Element._attributeTranslations.read.names),
+    values: {
+      checked: function(element, value) {
+        element.checked = !!value;
+      },
+
+      style: function(element, value) {
+        element.style.cssText = value ? value : '';
+      }
+    }
+  };
+
+  Element._attributeTranslations.has = {};
+
+  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
+      'encType maxLength readOnly longDesc').each(function(attr) {
+    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
+    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
+  });
+
+  (function(v) {
+    Object.extend(v, {
+      href:        v._getAttr,
+      src:         v._getAttr,
+      type:        v._getAttr,
+      action:      v._getAttrNode,
+      disabled:    v._flag,
+      checked:     v._flag,
+      readonly:    v._flag,
+      multiple:    v._flag,
+      onload:      v._getEv,
+      onunload:    v._getEv,
+      onclick:     v._getEv,
+      ondblclick:  v._getEv,
+      onmousedown: v._getEv,
+      onmouseup:   v._getEv,
+      onmouseover: v._getEv,
+      onmousemove: v._getEv,
+      onmouseout:  v._getEv,
+      onfocus:     v._getEv,
+      onblur:      v._getEv,
+      onkeypress:  v._getEv,
+      onkeydown:   v._getEv,
+      onkeyup:     v._getEv,
+      onsubmit:    v._getEv,
+      onreset:     v._getEv,
+      onselect:    v._getEv,
+      onchange:    v._getEv
+    });
+  })(Element._attributeTranslations.read.values);
+}
+
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1) ? 0.999999 :
+      (value === '') ? '' : (value < 0.00001) ? 0 : value;
+    return element;
+  };
+}
+
+else if (Prototype.Browser.WebKit) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+
+    if (value == 1)
+      if(element.tagName == 'IMG' && element.width) {
+        element.width++; element.width--;
+      } else try {
+        var n = document.createTextNode(' ');
+        element.appendChild(n);
+        element.removeChild(n);
+      } catch (e) { }
 
-/*--------------------------------------------------------------------------*/
+    return element;
+  };
+
+  // Safari returns margins on body which is incorrect if the child is absolutely
+  // positioned.  For performance reasons, redefine Position.cumulativeOffset for
+  // KHTML/WebKit only.
+  Element.Methods.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
 
-Abstract.Insertion = function(adjacency) {
-  this.adjacency = adjacency;
+    return Element._returnOffset(valueL, valueT);
+  };
 }
 
-Abstract.Insertion.prototype = {
-  initialize: function(element, content) {
-    this.element = $(element);
-    this.content = content.stripScripts();
+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
+  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
+  Element.Methods.update = function(element, content) {
+    element = $(element);
 
-    if (this.adjacency && this.element.insertAdjacentHTML) {
-      try {
-        this.element.insertAdjacentHTML(this.adjacency, this.content);
-      } catch (e) {
-        if (this.element.tagName.toLowerCase() == 'tbody') {
-          this.insertContent(this.contentFromAnonymousTable());
-        } else {
-          throw e;
-        }
-      }
-    } else {
-      this.range = this.element.ownerDocument.createRange();
-      if (this.initializeRange) this.initializeRange();
-      this.insertContent([this.range.createContextualFragment(this.content)]);
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+
+    content = Object.toHTML(content);
+    var tagName = element.tagName.toUpperCase();
+
+    if (tagName in Element._insertionTranslations.tags) {
+      $A(element.childNodes).each(function(node) { element.removeChild(node) });
+      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+        .each(function(node) { element.appendChild(node) });
     }
+    else element.innerHTML = content.stripScripts();
 
-    setTimeout(function() {content.evalScripts()}, 10);
-  },
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
+}
 
-  contentFromAnonymousTable: function() {
-    var div = document.createElement('div');
-    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
-    return $A(div.childNodes[0].childNodes[0].childNodes);
-  }
+if (document.createElement('div').outerHTML) {
+  Element.Methods.replace = function(element, content) {
+    element = $(element);
+
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) {
+      element.parentNode.replaceChild(content, element);
+      return element;
+    }
+
+    content = Object.toHTML(content);
+    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+    if (Element._insertionTranslations.tags[tagName]) {
+      var nextSibling = element.next();
+      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+      parent.removeChild(element);
+      if (nextSibling)
+        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+      else
+        fragments.each(function(node) { parent.appendChild(node) });
+    }
+    else element.outerHTML = content.stripScripts();
+
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
 }
 
-var Insertion = new Object();
+Element._returnOffset = function(l, t) {
+  var result = [l, t];
+  result.left = l;
+  result.top = t;
+  return result;
+};
 
-Insertion.Before = Class.create();
-Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
-  initializeRange: function() {
-    this.range.setStartBefore(this.element);
-  },
+Element._getContentFromAnonymousElement = function(tagName, html) {
+  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
+  div.innerHTML = t[0] + html + t[1];
+  t[2].times(function() { div = div.firstChild });
+  return $A(div.childNodes);
+};
 
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.parentNode.insertBefore(fragment, this.element);
-    }).bind(this));
+Element._insertionTranslations = {
+  before: {
+    adjacency: 'beforeBegin',
+    insert: function(element, node) {
+      element.parentNode.insertBefore(node, element);
+    },
+    initializeRange: function(element, range) {
+      range.setStartBefore(element);
+    }
+  },
+  top: {
+    adjacency: 'afterBegin',
+    insert: function(element, node) {
+      element.insertBefore(node, element.firstChild);
+    },
+    initializeRange: function(element, range) {
+      range.selectNodeContents(element);
+      range.collapse(true);
+    }
+  },
+  bottom: {
+    adjacency: 'beforeEnd',
+    insert: function(element, node) {
+      element.appendChild(node);
+    }
+  },
+  after: {
+    adjacency: 'afterEnd',
+    insert: function(element, node) {
+      element.parentNode.insertBefore(node, element.nextSibling);
+    },
+    initializeRange: function(element, range) {
+      range.setStartAfter(element);
+    }
+  },
+  tags: {
+    TABLE:  ['<table>',                '</table>',                   1],
+    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
+    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
+    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+    SELECT: ['<select>',               '</select>',                  1]
   }
-});
+};
 
-Insertion.Top = Class.create();
-Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(true);
-  },
+(function() {
+  this.bottom.initializeRange = this.top.initializeRange;
+  Object.extend(this.tags, {
+    THEAD: this.tags.TBODY,
+    TFOOT: this.tags.TBODY,
+    TH:    this.tags.TD
+  });
+}).call(Element._insertionTranslations);
 
-  insertContent: function(fragments) {
-    fragments.reverse(false).each((function(fragment) {
-      this.element.insertBefore(fragment, this.element.firstChild);
-    }).bind(this));
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    attribute = Element._attributeTranslations.has[attribute] || attribute;
+    var node = $(element).getAttributeNode(attribute);
+    return node && node.specified;
   }
-});
+};
 
-Insertion.Bottom = Class.create();
-Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(this.element);
-  },
+Element.Methods.ByTag = { };
 
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.appendChild(fragment);
-    }).bind(this));
-  }
-});
+Object.extend(Element, Element.Methods);
 
-Insertion.After = Class.create();
-Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
-  initializeRange: function() {
-    this.range.setStartAfter(this.element);
-  },
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+    document.createElement('div').__proto__) {
+  window.HTMLElement = { };
+  window.HTMLElement.prototype = document.createElement('div').__proto__;
+  Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.extend = (function() {
+  if (Prototype.BrowserFeatures.SpecificElementExtensions)
+    return Prototype.K;
+
+  var Methods = { }, ByTag = Element.Methods.ByTag;
+
+  var extend = Object.extend(function(element) {
+    if (!element || element._extendedByPrototype ||
+        element.nodeType != 1 || element == window) return element;
+
+    var methods = Object.clone(Methods),
+      tagName = element.tagName, property, value;
+
+    // extend methods for specific tags
+    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+    for (property in methods) {
+      value = methods[property];
+      if (Object.isFunction(value) && !(property in element))
+        element[property] = value.methodize();
+    }
+
+    element._extendedByPrototype = Prototype.emptyFunction;
+    return element;
+
+  }, {
+    refresh: function() {
+      // extend methods for all tags (Safari doesn't need this)
+      if (!Prototype.BrowserFeatures.ElementExtensions) {
+        Object.extend(Methods, Element.Methods);
+        Object.extend(Methods, Element.Methods.Simulated);
+      }
+    }
+  });
+
+  extend.refresh();
+  return extend;
+})();
+
+Element.hasAttribute = function(element, attribute) {
+  if (element.hasAttribute) return element.hasAttribute(attribute);
+  return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
 
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.parentNode.insertBefore(fragment,
-        this.element.nextSibling);
-    }).bind(this));
+Element.addMethods = function(methods) {
+  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+  if (!methods) {
+    Object.extend(Form, Form.Methods);
+    Object.extend(Form.Element, Form.Element.Methods);
+    Object.extend(Element.Methods.ByTag, {
+      "FORM":     Object.clone(Form.Methods),
+      "INPUT":    Object.clone(Form.Element.Methods),
+      "SELECT":   Object.clone(Form.Element.Methods),
+      "TEXTAREA": Object.clone(Form.Element.Methods)
+    });
   }
-});
 
-/*--------------------------------------------------------------------------*/
+  if (arguments.length == 2) {
+    var tagName = methods;
+    methods = arguments[1];
+  }
+
+  if (!tagName) Object.extend(Element.Methods, methods || { });
+  else {
+    if (Object.isArray(tagName)) tagName.each(extend);
+    else extend(tagName);
+  }
+
+  function extend(tagName) {
+    tagName = tagName.toUpperCase();
+    if (!Element.Methods.ByTag[tagName])
+      Element.Methods.ByTag[tagName] = { };
+    Object.extend(Element.Methods.ByTag[tagName], methods);
+  }
+
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    for (var property in methods) {
+      var value = methods[property];
+      // don't copy update, temporarily 
+      if (!Object.isFunction(value) || property == 'update') continue;
+      if (!onlyIfAbsent || !(property in destination))
+        destination[property] = value.methodize();
+    }
+  }
+
+  function findDOMClass(tagName) {
+    var klass;
+    var trans = {
+      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+      "FrameSet", "IFRAME": "IFrame"
+    };
+    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName.capitalize() + 'Element';
+    if (window[klass]) return window[klass];
+
+    window[klass] = { };
+    window[klass].prototype = document.createElement(tagName).__proto__;
+    return window[klass];
+  }
+
+  if (F.ElementExtensions) {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+  }
+
+  if (F.SpecificElementExtensions) {
+    for (var tag in Element.Methods.ByTag) {
+      var klass = findDOMClass(tag);
+      if (Object.isUndefined(klass)) continue;
+      copy(T[tag], klass.prototype);
+    }
+  }
 
-Element.ClassNames = Class.create();
-Element.ClassNames.prototype = {
-  initialize: function(element) {
-    this.element = $(element);
+  Object.extend(Element, Element.Methods);
+  delete Element.ByTag;
+
+  if (Element.extend.refresh) Element.extend.refresh();
+  Element.cache = { };
+};
+
+document.viewport = {
+  getDimensions: function() {
+    var dimensions = { };
+    $w('width height').each(function(d) {
+      var D = d.capitalize();
+      dimensions[d] = self['inner' + D] ||
+       (document.documentElement['client' + D] || document.body['client' + D]);
+    });
+    return dimensions;
   },
 
-  _each: function(iterator) {
-    this.element.className.split(/\s+/).select(function(name) {
-      return name.length > 0;
-    })._each(iterator);
+  getWidth: function() {
+    return this.getDimensions().width;
   },
 
-  set: function(className) {
-    this.element.className = className;
+  getHeight: function() {
+    return this.getDimensions().height;
   },
 
-  add: function(classNameToAdd) {
-    if (this.include(classNameToAdd)) return;
-    this.set(this.toArray().concat(classNameToAdd).join(' '));
+  getScrollOffsets: function() {
+    return Element._returnOffset(
+      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
+  }
+};
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license.  Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create({
+  initialize: function(expression) {
+    this.expression = expression.strip();
+    this.compileMatcher();
+  },
+
+  compileMatcher: function() {
+    // Selectors with namespaced attributes can't use the XPath version
+    if (Prototype.BrowserFeatures.XPath && !(/(\[[\w-]*?:|:checked)/).test(this.expression))
+      return this.compileXPathMatcher();
+
+    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+        c = Selector.criteria, le, p, m;
+
+    if (Selector._cache[e]) {
+      this.matcher = Selector._cache[e];
+      return;
+    }
+
+    this.matcher = ["this.matcher = function(root) {",
+                    "var r = root, h = Selector.handlers, c = false, n;"];
+
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
+    	      new Template(c[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.matcher.push("return h.unique(n);\n}");
+    eval(this.matcher.join('\n'));
+    Selector._cache[this.expression] = this.matcher;
+  },
+
+  compileXPathMatcher: function() {
+    var e = this.expression, ps = Selector.patterns,
+        x = Selector.xpath, le, m;
+
+    if (Selector._cache[e]) {
+      this.xpath = Selector._cache[e]; return;
+    }
+
+    this.matcher = ['.//*'];
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        if (m = e.match(ps[i])) {
+          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
+            new Template(x[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.xpath = this.matcher.join('');
+    Selector._cache[this.expression] = this.xpath;
   },
 
-  remove: function(classNameToRemove) {
-    if (!this.include(classNameToRemove)) return;
-    this.set(this.select(function(className) {
-      return className != classNameToRemove;
-    }).join(' '));
+  findElements: function(root) {
+    root = root || document;
+    if (this.xpath) return document._getElementsByXPath(this.xpath, root);
+    return this.matcher(root);
+  },
+
+  match: function(element) {
+    this.tokens = [];
+
+    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
+    var le, p, m;
+
+    while (e && le !== e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          // use the Selector.assertions methods unless the selector
+          // is too complex.
+          if (as[i]) {
+            this.tokens.push([i, Object.clone(m)]);
+            e = e.replace(m[0], '');
+          } else {
+            // reluctantly do a document-wide search
+            // and look for a match in the array
+            return this.findElements(document).include(element);
+          }
+        }
+      }
+    }
+
+    var match = true, name, matches;
+    for (var i = 0, token; token = this.tokens[i]; i++) {
+      name = token[0], matches = token[1];
+      if (!Selector.assertions[name](element, matches)) {
+        match = false; break;
+      }
+    }
+
+    return match;
   },
 
   toString: function() {
-    return this.toArray().join(' ');
+    return this.expression;
+  },
+
+  inspect: function() {
+    return "#<Selector:" + this.expression.inspect() + ">";
   }
-}
+});
 
-Object.extend(Element.ClassNames.prototype, Enumerable);
-var Field = {
-  clear: function() {
-    for (var i = 0; i < arguments.length; i++)
-      $(arguments[i]).value = '';
+Object.extend(Selector, {
+  _cache: { },
+
+  xpath: {
+    descendant:   "//*",
+    child:        "/*",
+    adjacent:     "/following-sibling::*[1]",
+    laterSibling: '/following-sibling::*',
+    tagName:      function(m) {
+      if (m[1] == '*') return '';
+      return "[local-name()='" + m[1].toLowerCase() +
+             "' or local-name()='" + m[1].toUpperCase() + "']";
+    },
+    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+    id:           "[@id='#{1}']",
+    attrPresence: "[@#{1}]",
+    attr: function(m) {
+      m[3] = m[5] || m[6];
+      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+    },
+    pseudo: function(m) {
+      var h = Selector.xpath.pseudos[m[1]];
+      if (!h) return '';
+      if (Object.isFunction(h)) return h(m);
+      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+    },
+    operators: {
+      '=':  "[@#{1}='#{3}']",
+      '!=': "[@#{1}!='#{3}']",
+      '^=': "[starts-with(@#{1}, '#{3}')]",
+      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+      '*=': "[contains(@#{1}, '#{3}')]",
+      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+    },
+    pseudos: {
+      'first-child': '[not(preceding-sibling::*)]',
+      'last-child':  '[not(following-sibling::*)]',
+      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
+      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+      'checked':     "[@checked]",
+      'disabled':    "[@disabled]",
+      'enabled':     "[not(@disabled)]",
+      'not': function(m) {
+        var e = m[6], p = Selector.patterns,
+            x = Selector.xpath, le, m, v;
+
+        var exclusion = [];
+        while (e && le != e && (/\S/).test(e)) {
+          le = e;
+          for (var i in p) {
+            if (m = e.match(p[i])) {
+              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
+              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+              e = e.replace(m[0], '');
+              break;
+            }
+          }
+        }
+        return "[not(" + exclusion.join(" and ") + ")]";
+      },
+      'nth-child':      function(m) {
+        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+      },
+      'nth-last-child': function(m) {
+        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+      },
+      'nth-of-type':    function(m) {
+        return Selector.xpath.pseudos.nth("position() ", m);
+      },
+      'nth-last-of-type': function(m) {
+        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+      },
+      'first-of-type':  function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+      },
+      'last-of-type':   function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+      },
+      'only-of-type':   function(m) {
+        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+      },
+      nth: function(fragment, m) {
+        var mm, formula = m[6], predicate;
+        if (formula == 'even') formula = '2n+0';
+        if (formula == 'odd')  formula = '2n+1';
+        if (mm = formula.match(/^(\d+)$/)) // digit only
+          return '[' + fragment + "= " + mm[1] + ']';
+        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+          if (mm[1] == "-") mm[1] = -1;
+          var a = mm[1] ? Number(mm[1]) : 1;
+          var b = mm[2] ? Number(mm[2]) : 0;
+          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+          "((#{fragment} - #{b}) div #{a} >= 0)]";
+          return new Template(predicate).evaluate({
+            fragment: fragment, a: a, b: b });
+        }
+      }
+    }
   },
 
-  focus: function(element) {
-    $(element).focus();
+  criteria: {
+    tagName:      'n = h.tagName(n, r, "#{1}", c);   c = false;',
+    className:    'n = h.className(n, r, "#{1}", c); c = false;',
+    id:           'n = h.id(n, r, "#{1}", c);        c = false;',
+    attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+    attr: function(m) {
+      m[3] = (m[5] || m[6]);
+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+    },
+    pseudo: function(m) {
+      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+    },
+    descendant:   'c = "descendant";',
+    child:        'c = "child";',
+    adjacent:     'c = "adjacent";',
+    laterSibling: 'c = "laterSibling";'
+  },
+
+  patterns: {
+    // combinators must be listed first
+    // (and descendant needs to be last combinator)
+    laterSibling: /^\s*~\s*/,
+    child:        /^\s*>\s*/,
+    adjacent:     /^\s*\+\s*/,
+    descendant:   /^\s/,
+
+    // selectors follow
+    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
+    id:           /^#([\w\-\*]+)(\b|$)/,
+    className:    /^\.([\w\-\*]+)(\b|$)/,
+    pseudo:       /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/,
+    attrPresence: /^\[([\w]+)\]/,
+    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+  },
+
+  // for Selector.match and Element#match
+  assertions: {
+    tagName: function(element, matches) {
+      return matches[1].toUpperCase() == element.tagName.toUpperCase();
+    },
+
+    className: function(element, matches) {
+      return Element.hasClassName(element, matches[1]);
+    },
+
+    id: function(element, matches) {
+      return element.id === matches[1];
+    },
+
+    attrPresence: function(element, matches) {
+      return Element.hasAttribute(element, matches[1]);
+    },
+
+    attr: function(element, matches) {
+      var nodeValue = Element.readAttribute(element, matches[1]);
+      return Selector.operators[matches[2]](nodeValue, matches[3]);
+    }
+  },
+
+  handlers: {
+    // UTILITY FUNCTIONS
+    // joins two collections
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        a.push(node);
+      return a;
+    },
+
+    // marks an array of nodes for counting
+    mark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._counted = true;
+      return nodes;
+    },
+
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._counted = undefined;
+      return nodes;
+    },
+
+    // mark each child node with its position (for nth calls)
+    // "ofType" flag indicates whether we're indexing for nth-of-type
+    // rather than nth-child
+    index: function(parentNode, reverse, ofType) {
+      parentNode._counted = true;
+      if (reverse) {
+        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+          var node = nodes[i];
+          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+        }
+      } else {
+        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+      }
+    },
+
+    // filters out duplicates and extends all nodes
+    unique: function(nodes) {
+      if (nodes.length == 0) return nodes;
+      var results = [], n;
+      for (var i = 0, l = nodes.length; i < l; i++)
+        if (!(n = nodes[i])._counted) {
+          n._counted = true;
+          results.push(Element.extend(n));
+        }
+      return Selector.handlers.unmark(results);
+    },
+
+    // COMBINATOR FUNCTIONS
+    descendant: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, node.getElementsByTagName('*'));
+      return results;
+    },
+
+    child: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
+          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+      }
+      return results;
+    },
+
+    adjacent: function(nodes) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        var next = this.nextElementSibling(node);
+        if (next) results.push(next);
+      }
+      return results;
+    },
+
+    laterSibling: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, Element.nextSiblings(node));
+      return results;
+    },
+
+    nextElementSibling: function(node) {
+      while (node = node.nextSibling)
+	      if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    previousElementSibling: function(node) {
+      while (node = node.previousSibling)
+        if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    // TOKEN FUNCTIONS
+    tagName: function(nodes, root, tagName, combinator) {
+      tagName = tagName.toUpperCase();
+      var results = [], h = Selector.handlers;
+      if (nodes) {
+        if (combinator) {
+          // fastlane for ordinary descendant combinators
+          if (combinator == "descendant") {
+            for (var i = 0, node; node = nodes[i]; i++)
+              h.concat(results, node.getElementsByTagName(tagName));
+            return results;
+          } else nodes = this[combinator](nodes);
+          if (tagName == "*") return nodes;
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.tagName.toUpperCase() == tagName) results.push(node);
+        return results;
+      } else return root.getElementsByTagName(tagName);
+    },
+
+    id: function(nodes, root, id, combinator) {
+      var targetNode = $(id), h = Selector.handlers;
+      if (!targetNode) return [];
+      if (!nodes && root == document) return [targetNode];
+      if (nodes) {
+        if (combinator) {
+          if (combinator == 'child') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (targetNode.parentNode == node) return [targetNode];
+          } else if (combinator == 'descendant') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Element.descendantOf(targetNode, node)) return [targetNode];
+          } else if (combinator == 'adjacent') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Selector.handlers.previousElementSibling(targetNode) == node)
+                return [targetNode];
+          } else nodes = h[combinator](nodes);
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node == targetNode) return [targetNode];
+        return [];
+      }
+      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+    },
+
+    className: function(nodes, root, className, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      return Selector.handlers.byClassName(nodes, root, className);
+    },
+
+    byClassName: function(nodes, root, className) {
+      if (!nodes) nodes = Selector.handlers.descendant([root]);
+      var needle = ' ' + className + ' ';
+      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+        nodeClassName = node.className;
+        if (nodeClassName.length == 0) continue;
+        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+          results.push(node);
+      }
+      return results;
+    },
+
+    attrPresence: function(nodes, root, attr) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      var results = [];
+      for (var i = 0, node; node = nodes[i]; i++)
+        if (Element.hasAttribute(node, attr)) results.push(node);
+      return results;
+    },
+
+    attr: function(nodes, root, attr, value, operator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      var handler = Selector.operators[operator], results = [];
+      for (var i = 0, node; node = nodes[i]; i++) {
+        var nodeValue = Element.readAttribute(node, attr);
+        if (nodeValue === null) continue;
+        if (handler(nodeValue, value)) results.push(node);
+      }
+      return results;
+    },
+
+    pseudo: function(nodes, name, value, root, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      return Selector.pseudos[name](nodes, value, root);
+    }
   },
 
-  present: function() {
-    for (var i = 0; i < arguments.length; i++)
-      if ($(arguments[i]).value == '') return false;
-    return true;
+  pseudos: {
+    'first-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.previousElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'last-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.nextElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'only-child': function(nodes, value, root) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+          results.push(node);
+      return results;
+    },
+    'nth-child':        function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root);
+    },
+    'nth-last-child':   function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true);
+    },
+    'nth-of-type':      function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, false, true);
+    },
+    'nth-last-of-type': function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true, true);
+    },
+    'first-of-type':    function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, false, true);
+    },
+    'last-of-type':     function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, true, true);
+    },
+    'only-of-type':     function(nodes, formula, root) {
+      var p = Selector.pseudos;
+      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+    },
+
+    // handles the an+b logic
+    getIndices: function(a, b, total) {
+      if (a == 0) return b > 0 ? [b] : [];
+      return $R(1, total).inject([], function(memo, i) {
+        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+        return memo;
+      });
+    },
+
+    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+    nth: function(nodes, formula, root, reverse, ofType) {
+      if (nodes.length == 0) return [];
+      if (formula == 'even') formula = '2n+0';
+      if (formula == 'odd')  formula = '2n+1';
+      var h = Selector.handlers, results = [], indexed = [], m;
+      h.mark(nodes);
+      for (var i = 0, node; node = nodes[i]; i++) {
+        if (!node.parentNode._counted) {
+          h.index(node.parentNode, reverse, ofType);
+          indexed.push(node.parentNode);
+        }
+      }
+      if (formula.match(/^\d+$/)) { // just a number
+        formula = Number(formula);
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.nodeIndex == formula) results.push(node);
+      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+        if (m[1] == "-") m[1] = -1;
+        var a = m[1] ? Number(m[1]) : 1;
+        var b = m[2] ? Number(m[2]) : 0;
+        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+          for (var j = 0; j < l; j++)
+            if (node.nodeIndex == indices[j]) results.push(node);
+        }
+      }
+      h.unmark(nodes);
+      h.unmark(indexed);
+      return results;
+    },
+
+    'empty': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        // IE treats comments as element nodes
+        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+        results.push(node);
+      }
+      return results;
+    },
+
+    'not': function(nodes, selector, root) {
+      var h = Selector.handlers, selectorType, m;
+      var exclusions = new Selector(selector).findElements(root);
+      h.mark(exclusions);
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node._counted) results.push(node);
+      h.unmark(exclusions);
+      return results;
+    },
+
+    'enabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node.disabled) results.push(node);
+      return results;
+    },
+
+    'disabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.disabled) results.push(node);
+      return results;
+    },
+
+    'checked': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.checked) results.push(node);
+      return results;
+    }
   },
 
-  select: function(element) {
-    $(element).select();
+  operators: {
+    '=':  function(nv, v) { return nv == v; },
+    '!=': function(nv, v) { return nv != v; },
+    '^=': function(nv, v) { return nv.startsWith(v); },
+    '$=': function(nv, v) { return nv.endsWith(v); },
+    '*=': function(nv, v) { return nv.include(v); },
+    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+  },
+
+  matchElements: function(elements, expression) {
+    var matches = new Selector(expression).findElements(), h = Selector.handlers;
+    h.mark(matches);
+    for (var i = 0, results = [], element; element = elements[i]; i++)
+      if (element._counted) results.push(element);
+    h.unmark(matches);
+    return results;
   },
 
-  activate: function(element) {
-    element = $(element);
-    element.focus();
-    if (element.select)
-      element.select();
-  }
-}
+  findElement: function(elements, expression, index) {
+    if (Object.isNumber(expression)) {
+      index = expression; expression = false;
+    }
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
 
-/*--------------------------------------------------------------------------*/
+  findChildElements: function(element, expressions) {
+    var exprs = expressions.join(','), expressions = [];
+    exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+      expressions.push(m[1].strip());
+    });
+    var results = [], h = Selector.handlers;
+    for (var i = 0, l = expressions.length, selector; i < l; i++) {
+      selector = new Selector(expressions[i].strip());
+      h.concat(results, selector.findElements(element));
+    }
+    return (l > 1) ? h.unique(results) : results;
+  }
+});
 
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
 var Form = {
-  serialize: function(form) {
-    var elements = Form.getElements($(form));
-    var queryComponents = new Array();
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  },
 
-    for (var i = 0; i < elements.length; i++) {
-      var queryComponent = Form.Element.serialize(elements[i]);
-      if (queryComponent)
-        queryComponents.push(queryComponent);
-    }
+  serializeElements: function(elements, options) {
+    if (typeof options != 'object') options = { hash: !!options };
+    else if (options.hash === undefined) options.hash = true;
+    var key, value, submitted = false, submit = options.submit;
+
+    var data = elements.inject({ }, function(result, element) {
+      if (!element.disabled && element.name) {
+        key = element.name; value = $(element).getValue();
+        if (value != null && (element.type != 'submit' || (!submitted &&
+            submit !== false && (!submit || key == submit) && (submitted = true)))) {
+          if (key in result) {
+            // a key is already present; construct an array of values
+            if (!Object.isArray(result[key])) result[key] = [result[key]];
+            result[key].push(value);
+          }
+          else result[key] = value;
+        }
+      }
+      return result;
+    });
+
+    return options.hash ? data : Object.toQueryString(data);
+  }
+};
 
-    return queryComponents.join('&');
+Form.Methods = {
+  serialize: function(form, options) {
+    return Form.serializeElements(Form.getElements(form), options);
   },
 
   getElements: function(form) {
-    form = $(form);
-    var elements = new Array();
-
-    for (tagName in Form.Element.Serializers) {
-      var tagElements = form.getElementsByTagName(tagName);
-      for (var j = 0; j < tagElements.length; j++)
-        elements.push(tagElements[j]);
-    }
-    return elements;
+    return $A($(form).getElementsByTagName('*')).inject([],
+      function(elements, child) {
+        if (Form.Element.Serializers[child.tagName.toLowerCase()])
+          elements.push(Element.extend(child));
+        return elements;
+      }
+    );
   },
 
   getInputs: function(form, typeName, name) {
     form = $(form);
     var inputs = form.getElementsByTagName('input');
 
-    if (!typeName && !name)
-      return inputs;
+    if (!typeName && !name) return $A(inputs).map(Element.extend);
 
-    var matchingInputs = new Array();
-    for (var i = 0; i < inputs.length; i++) {
+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
       var input = inputs[i];
-      if ((typeName && input.type != typeName) ||
-          (name && input.name != name))
+      if ((typeName && input.type != typeName) || (name && input.name != name))
         continue;
-      matchingInputs.push(input);
+      matchingInputs.push(Element.extend(input));
     }
 
     return matchingInputs;
   },
 
   disable: function(form) {
-    var elements = Form.getElements(form);
-    for (var i = 0; i < elements.length; i++) {
-      var element = elements[i];
-      element.blur();
-      element.disabled = 'true';
-    }
+    form = $(form);
+    Form.getElements(form).invoke('disable');
+    return form;
   },
 
   enable: function(form) {
-    var elements = Form.getElements(form);
-    for (var i = 0; i < elements.length; i++) {
-      var element = elements[i];
-      element.disabled = '';
-    }
+    form = $(form);
+    Form.getElements(form).invoke('enable');
+    return form;
   },
 
   findFirstElement: function(form) {
-    return Form.getElements(form).find(function(element) {
-      return element.type != 'hidden' && !element.disabled &&
-        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    var elements = $(form).getElements().findAll(function(element) {
+      return 'hidden' != element.type && !element.disabled;
+    });
+    var firstByIndex = elements.findAll(function(element) {
+      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+    }).sortBy(function(element) { return element.tabIndex }).first();
+
+    return firstByIndex ? firstByIndex : elements.find(function(element) {
+      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
     });
   },
 
   focusFirstElement: function(form) {
-    Field.activate(Form.findFirstElement(form));
+    form = $(form);
+    form.findFirstElement().activate();
+    return form;
   },
 
-  reset: function(form) {
-    $(form).reset();
+  request: function(form, options) {
+    form = $(form), options = Object.clone(options || { });
+
+    var params = options.parameters, action = form.readAttribute('action') || '';
+    if (action.blank()) action = window.location.href;
+    options.parameters = form.serialize(true);
+
+    if (params) {
+      if (Object.isString(params)) params = params.toQueryParams();
+      Object.extend(options.parameters, params);
+    }
+
+    if (form.hasAttribute('method') && !options.method)
+      options.method = form.method;
+
+    return new Ajax.Request(action, options);
   }
-}
+};
 
-Form.Element = {
-  serialize: function(element) {
-    element = $(element);
-    var method = element.tagName.toLowerCase();
-    var parameter = Form.Element.Serializers[method](element);
+/*--------------------------------------------------------------------------*/
 
-    if (parameter) {
-      var key = encodeURIComponent(parameter[0]);
-      if (key.length == 0) return;
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
 
-      if (parameter[1].constructor != Array)
-        parameter[1] = [parameter[1]];
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+};
 
-      return parameter[1].map(function(value) {
-        return key + '=' + encodeURIComponent(value);
-      }).join('&');
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    if (!element.disabled && element.name) {
+      var value = element.getValue();
+      if (value != undefined) {
+        var pair = { };
+        pair[element.name] = value;
+        return Object.toQueryString(pair);
+      }
     }
+    return '';
   },
 
   getValue: function(element) {
     element = $(element);
     var method = element.tagName.toLowerCase();
-    var parameter = Form.Element.Serializers[method](element);
+    return Form.Element.Serializers[method](element);
+  },
 
-    if (parameter)
-      return parameter[1];
-  }
-}
+  setValue: function(element, value) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    Form.Element.Serializers[method](element, value);
+    return element;
+  },
 
-Form.Element.Serializers = {
-  input: function(element) {
-    switch (element.type.toLowerCase()) {
-      case 'submit':
-      case 'hidden':
-      case 'password':
-      case 'text':
-        return Form.Element.Serializers.textarea(element);
-      case 'checkbox':
-      case 'radio':
-        return Form.Element.Serializers.inputSelector(element);
-    }
-    return false;
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    try {
+      element.focus();
+      if (element.select && (element.tagName.toLowerCase() != 'input' ||
+          !['button', 'reset', 'submit'].include(element.type)))
+        element.select();
+    } catch (e) { }
+    return element;
   },
 
-  inputSelector: function(element) {
-    if (element.checked)
-      return [element.name, element.value];
+  disable: function(element) {
+    element = $(element);
+    element.blur();
+    element.disabled = true;
+    return element;
   },
 
-  textarea: function(element) {
-    return [element.name, element.value];
+  enable: function(element) {
+    element = $(element);
+    element.disabled = false;
+    return element;
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element, value) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element, value);
+      default:
+        return Form.Element.Serializers.textarea(element, value);
+    }
   },
 
-  select: function(element) {
-    return Form.Element.Serializers[element.type == 'select-one' ?
-      'selectOne' : 'selectMany'](element);
+  inputSelector: function(element, value) {
+    if (value === undefined) return element.checked ? element.value : null;
+    else element.checked = !!value;
+  },
+
+  textarea: function(element, value) {
+    if (value === undefined) return element.value;
+    else element.value = value;
+  },
+
+  select: function(element, index) {
+    if (index === undefined)
+      return this[element.type == 'select-one' ?
+        'selectOne' : 'selectMany'](element);
+    else {
+      var opt, value, single = !Object.isArray(index);
+      for (var i = 0, length = element.length; i < length; i++) {
+        opt = element.options[i];
+        value = this.optionValue(opt);
+        if (single) {
+          if (value == index) {
+            opt.selected = true;
+            return;
+          }
+        }
+        else opt.selected = index.include(value);
+      }
+    }
   },
 
   selectOne: function(element) {
-    var value = '', opt, index = element.selectedIndex;
-    if (index >= 0) {
-      opt = element.options[index];
-      value = opt.value;
-      if (!value && !('value' in opt))
-        value = opt.text;
-    }
-    return [element.name, value];
+    var index = element.selectedIndex;
+    return index >= 0 ? this.optionValue(element.options[index]) : null;
   },
 
   selectMany: function(element) {
-    var value = new Array();
-    for (var i = 0; i < element.length; i++) {
+    var values, length = element.length;
+    if (!length) return null;
+
+    for (var i = 0, values = []; i < length; i++) {
       var opt = element.options[i];
-      if (opt.selected) {
-        var optValue = opt.value;
-        if (!optValue && !('value' in opt))
-          optValue = opt.text;
-        value.push(optValue);
-      }
+      if (opt.selected) values.push(this.optionValue(opt));
     }
-    return [element.name, value];
-  }
-}
-
-/*--------------------------------------------------------------------------*/
+    return values;
+  },
 
-var $F = Form.Element.getValue;
+  optionValue: function(opt) {
+    // extend element because hasAttribute may not be native
+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+  }
+};
 
 /*--------------------------------------------------------------------------*/
 
-Abstract.TimedObserver = function() {}
-Abstract.TimedObserver.prototype = {
-  initialize: function(element, frequency, callback) {
-    this.frequency = frequency;
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+  initialize: function($super, element, frequency, callback) {
+    $super(callback, frequency);
     this.element   = $(element);
-    this.callback  = callback;
-
     this.lastValue = this.getValue();
-    this.registerCallback();
   },
 
-  registerCallback: function() {
-    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
-  },
-
-  onTimerEvent: function() {
+  execute: function() {
     var value = this.getValue();
-    if (this.lastValue != value) {
+    if (Object.isString(this.lastValue) && Object.isString(value) ?
+        this.lastValue != value : String(this.lastValue) != String(value)) {
       this.callback(this.element, value);
       this.lastValue = value;
     }
   }
-}
+});
 
-Form.Element.Observer = Class.create();
-Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
   getValue: function() {
     return Form.Element.getValue(this.element);
   }
 });
 
-Form.Observer = Class.create();
-Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+Form.Observer = Class.create(Abstract.TimedObserver, {
   getValue: function() {
     return Form.serialize(this.element);
   }
@@ -1391,8 +3638,7 @@
 
 /*--------------------------------------------------------------------------*/
 
-Abstract.EventObserver = function() {}
-Abstract.EventObserver.prototype = {
+Abstract.EventObserver = Class.create({
   initialize: function(element, callback) {
     this.element  = $(element);
     this.callback = callback;
@@ -1413,9 +3659,7 @@
   },
 
   registerFormCallbacks: function() {
-    var elements = Form.getElements(this.element);
-    for (var i = 0; i < elements.length; i++)
-      this.registerCallback(elements[i]);
+    Form.getElements(this.element).each(this.registerCallback, this);
   },
 
   registerCallback: function(element) {
@@ -1425,34 +3669,26 @@
         case 'radio':
           Event.observe(element, 'click', this.onElementEvent.bind(this));
           break;
-        case 'password':
-        case 'text':
-        case 'textarea':
-        case 'select-one':
-        case 'select-multiple':
+        default:
           Event.observe(element, 'change', this.onElementEvent.bind(this));
           break;
       }
     }
   }
-}
+});
 
-Form.Element.EventObserver = Class.create();
-Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
   getValue: function() {
     return Form.Element.getValue(this.element);
   }
 });
 
-Form.EventObserver = Class.create();
-Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+Form.EventObserver = Class.create(Abstract.EventObserver, {
   getValue: function() {
     return Form.serialize(this.element);
   }
 });
-if (!window.Event) {
-  var Event = new Object();
-}
+if (!window.Event) var Event = { };
 
 Object.extend(Event, {
   KEY_BACKSPACE: 8,
@@ -1464,99 +3700,339 @@
   KEY_RIGHT:    39,
   KEY_DOWN:     40,
   KEY_DELETE:   46,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+  KEY_INSERT:   45,
+
+  cache: { },
+
+  relatedTarget: function(event) {
+    var element;
+    switch(event.type) {
+      case 'mouseover': element = event.fromElement; break;
+      case 'mouseout':  element = event.toElement;   break;
+      default: return null;
+    }
+    return Element.extend(element);
+  }
+});
 
-  element: function(event) {
-    return event.target || event.srcElement;
-  },
+Event.Methods = (function() {
+  var isButton;
 
-  isLeftClick: function(event) {
-    return (((event.which) && (event.which == 1)) ||
-            ((event.button) && (event.button == 1)));
-  },
+  if (Prototype.Browser.IE) {
+    var buttonMap = { 0: 1, 1: 4, 2: 2 };
+    isButton = function(event, code) {
+      return event.button == buttonMap[code];
+    };
+
+  } else if (Prototype.Browser.WebKit) {
+    isButton = function(event, code) {
+      switch (code) {
+        case 0: return event.which == 1 && !event.metaKey;
+        case 1: return event.which == 1 && event.metaKey;
+        default: return false;
+      }
+    };
 
-  pointerX: function(event) {
-    return event.pageX || (event.clientX +
-      (document.documentElement.scrollLeft || document.body.scrollLeft));
-  },
+  } else {
+    isButton = function(event, code) {
+      return event.which ? (event.which === code + 1) : (event.button === code);
+    };
+  }
+
+  return {
+    isLeftClick:   function(event) { return isButton(event, 0) },
+    isMiddleClick: function(event) { return isButton(event, 1) },
+    isRightClick:  function(event) { return isButton(event, 2) },
+
+    element: function(event) {
+      var node = Event.extend(event).target;
+      return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);
+    },
+
+    findElement: function(event, expression) {
+      var element = Event.element(event);
+      return element.match(expression) ? element : element.up(expression);
+    },
+
+    pointer: function(event) {
+      return {
+        x: event.pageX || (event.clientX +
+          (document.documentElement.scrollLeft || document.body.scrollLeft)),
+        y: event.pageY || (event.clientY +
+          (document.documentElement.scrollTop || document.body.scrollTop))
+      };
+    },
 
-  pointerY: function(event) {
-    return event.pageY || (event.clientY +
-      (document.documentElement.scrollTop || document.body.scrollTop));
-  },
+    pointerX: function(event) { return Event.pointer(event).x },
+    pointerY: function(event) { return Event.pointer(event).y },
 
-  stop: function(event) {
-    if (event.preventDefault) {
+    stop: function(event) {
+      Event.extend(event);
       event.preventDefault();
       event.stopPropagation();
-    } else {
-      event.returnValue = false;
-      event.cancelBubble = true;
+      event.stopped = true;
     }
-  },
+  };
+})();
 
-  // find the first node with the given tagName, starting from the
-  // node the event was triggered on; traverses the DOM upwards
-  findElement: function(event, tagName) {
-    var element = Event.element(event);
-    while (element.parentNode && (!element.tagName ||
-        (element.tagName.toUpperCase() != tagName.toUpperCase())))
-      element = element.parentNode;
-    return element;
-  },
+Event.extend = (function() {
+  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+    m[name] = Event.Methods[name].methodize();
+    return m;
+  });
+
+  if (Prototype.Browser.IE) {
+    Object.extend(methods, {
+      stopPropagation: function() { this.cancelBubble = true },
+      preventDefault:  function() { this.returnValue = false },
+      inspect: function() { return "[object Event]" }
+    });
+
+    return function(event) {
+      if (!event) return false;
+      if (event._extendedByPrototype) return event;
+
+      event._extendedByPrototype = Prototype.emptyFunction;
+      var pointer = Event.pointer(event);
+      Object.extend(event, {
+        target: event.srcElement,
+        relatedTarget: Event.relatedTarget(event),
+        pageX:  pointer.x,
+        pageY:  pointer.y
+      });
+      return Object.extend(event, methods);
+    };
+
+  } else {
+    Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__;
+    Object.extend(Event.prototype, methods);
+    return Prototype.K;
+  }
+})();
+
+Object.extend(Event, (function() {
+  var cache = Event.cache;
+
+  function getEventID(element) {
+    if (element._eventID) return element._eventID;
+    arguments.callee.id = arguments.callee.id || 1;
+    return element._eventID = ++arguments.callee.id;
+  }
+
+  function getDOMEventName(eventName) {
+    if (eventName && eventName.include(':')) return "dataavailable";
+    return eventName;
+  }
+
+  function getCacheForID(id) {
+    return cache[id] = cache[id] || { };
+  }
+
+  function getWrappersForEventName(id, eventName) {
+    var c = getCacheForID(id);
+    return c[eventName] = c[eventName] || [];
+  }
+
+  function createWrapper(element, eventName, handler) {
+    var id = getEventID(element);
+    var c = getWrappersForEventName(id, eventName);
+    if (c.pluck("handler").include(handler)) return false;
+
+    var wrapper = function(event) {
+      if (!Event || !Event.extend ||
+        (event.eventName && event.eventName != eventName))
+          return false;
+
+      Event.extend(event);
+      handler.call(element, event)
+    };
 
-  observers: false,
+    wrapper.handler = handler;
+    c.push(wrapper);
+    return wrapper;
+  }
+
+  function findWrapper(id, eventName, handler) {
+    var c = getWrappersForEventName(id, eventName);
+    return c.find(function(wrapper) { return wrapper.handler == handler });
+  }
+
+  function destroyWrapper(id, eventName, handler) {
+    var c = getCacheForID(id);
+    if (!c[eventName]) return false;
+    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
+  }
+
+  function destroyCache() {
+    for (var id in cache)
+      for (var eventName in cache[id])
+        cache[id][eventName] = null;
+  }
+
+  if (window.attachEvent) {
+    window.attachEvent("onunload", destroyCache);
+  }
+
+  return {
+    observe: function(element, eventName, handler) {
+      element = $(element);
+      var name = getDOMEventName(eventName);
+
+      var wrapper = createWrapper(element, eventName, handler);
+      if (!wrapper) return element;
+
+      if (element.addEventListener) {
+        element.addEventListener(name, wrapper, false);
+      } else {
+        element.attachEvent("on" + name, wrapper);
+      }
+
+      return element;
+    },
+
+    stopObserving: function(element, eventName, handler) {
+      element = $(element);
+      var id = getEventID(element), name = getDOMEventName(eventName);
+
+      if (!handler && eventName) {
+        getWrappersForEventName(id, eventName).each(function(wrapper) {
+          element.stopObserving(eventName, wrapper.handler);
+        });
+        return element;
+
+      } else if (!eventName) {
+        Object.keys(getCacheForID(id)).each(function(eventName) {
+          element.stopObserving(eventName);
+        });
+        return element;
+      }
+
+      var wrapper = findWrapper(id, eventName, handler);
+      if (!wrapper) return element;
+
+      if (element.removeEventListener) {
+        element.removeEventListener(name, wrapper, false);
+      } else {
+        element.detachEvent("on" + name, wrapper);
+      }
+
+      destroyWrapper(id, eventName, handler);
+
+      return element;
+    },
+
+    fire: function(element, eventName, memo) {
+      element = $(element);
+      if (element == document && document.createEvent && !element.dispatchEvent)
+        element = document.documentElement;
+
+      if (document.createEvent) {
+        var event = document.createEvent("HTMLEvents");
+        event.initEvent("dataavailable", true, true);
+      } else {
+        var event = document.createEventObject();
+        event.eventType = "ondataavailable";
+      }
+
+      event.eventName = eventName;
+      event.memo = memo || { };
 
-  _observeAndCache: function(element, name, observer, useCapture) {
-    if (!this.observers) this.observers = [];
-    if (element.addEventListener) {
-      this.observers.push([element, name, observer, useCapture]);
-      element.addEventListener(name, observer, useCapture);
-    } else if (element.attachEvent) {
-      this.observers.push([element, name, observer, useCapture]);
-      element.attachEvent('on' + name, observer);
+      if (document.createEvent) {
+        element.dispatchEvent(event);
+      } else {
+        element.fireEvent(event.eventType, event);
+      }
+
+      return event;
     }
-  },
+  };
+})());
+
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+  fire:          Event.fire,
+  observe:       Event.observe,
+  stopObserving: Event.stopObserving
+});
+
+Object.extend(document, {
+  fire:          Element.Methods.fire.methodize(),
+  observe:       Element.Methods.observe.methodize(),
+  stopObserving: Element.Methods.stopObserving.methodize()
+});
+
+(function() {
+  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+     Matthias Miller, Dean Edwards and John Resig. */
+
+  var timer, fired = false;
+
+  function fireContentLoadedEvent() {
+    if (fired) return;
+    if (timer) window.clearInterval(timer);
+    document.fire("dom:loaded");
+    fired = true;
+  }
+
+  if (document.addEventListener) {
+    if (Prototype.Browser.WebKit) {
+      timer = window.setInterval(function() {
+        if (/loaded|complete/.test(document.readyState))
+          fireContentLoadedEvent();
+      }, 0);
 
-  unloadCache: function() {
-    if (!Event.observers) return;
-    for (var i = 0; i < Event.observers.length; i++) {
-      Event.stopObserving.apply(this, Event.observers[i]);
-      Event.observers[i][0] = null;
+      Event.observe(window, "load", fireContentLoadedEvent);
+
+    } else {
+      document.addEventListener("DOMContentLoaded",
+        fireContentLoadedEvent, false);
     }
-    Event.observers = false;
-  },
 
-  observe: function(element, name, observer, useCapture) {
-    var element = $(element);
-    useCapture = useCapture || false;
+  } else {
+    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
+    $("__onDOMContentLoaded").onreadystatechange = function() {
+      if (this.readyState == "complete") {
+        this.onreadystatechange = null;
+        fireContentLoadedEvent();
+      }
+    };
+  }
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
+
+Hash.toQueryString = Object.toQueryString;
+
+var Toggle = { display: Element.toggle };
 
-    if (name == 'keypress' &&
-        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
-        || element.attachEvent))
-      name = 'keydown';
+Element.Methods.childOf = Element.Methods.descendantOf;
 
-    this._observeAndCache(element, name, observer, useCapture);
+var Insertion = {
+  Before: function(element, content) {
+    return Element.insert(element, {before:content});
   },
 
-  stopObserving: function(element, name, observer, useCapture) {
-    var element = $(element);
-    useCapture = useCapture || false;
+  Top: function(element, content) {
+    return Element.insert(element, {top:content});
+  },
 
-    if (name == 'keypress' &&
-        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
-        || element.detachEvent))
-      name = 'keydown';
+  Bottom: function(element, content) {
+    return Element.insert(element, {bottom:content});
+  },
 
-    if (element.removeEventListener) {
-      element.removeEventListener(name, observer, useCapture);
-    } else if (element.detachEvent) {
-      element.detachEvent('on' + name, observer);
-    }
+  After: function(element, content) {
+    return Element.insert(element, {after:content});
   }
-});
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
 
-/* prevent memory leaks in IE */
-Event.observe(window, 'unload', Event.unloadCache, false);
+// This should be moved to script.aculo.us; notice the deprecated methods
+// further below, that map to the newer Element methods.
 var Position = {
   // set to true if needed, warning: firefox performance problems
   // NOT neeeded for page scrolling, only if draggable contained in
@@ -1576,58 +4052,13 @@
                 || 0;
   },
 
-  realOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.scrollTop  || 0;
-      valueL += element.scrollLeft || 0;
-      element = element.parentNode;
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  cumulativeOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      element = element.offsetParent;
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  positionedOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      element = element.offsetParent;
-      if (element) {
-        p = Element.getStyle(element, 'position');
-        if (p == 'relative' || p == 'absolute') break;
-      }
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  offsetParent: function(element) {
-    if (element.offsetParent) return element.offsetParent;
-    if (element == document.body) return element;
-
-    while ((element = element.parentNode) && element != document.body)
-      if (Element.getStyle(element, 'position') != 'static')
-        return element;
-
-    return document.body;
-  },
-
   // caches x/y coordinate pair to use with overlap
   within: function(element, x, y) {
     if (this.includeScrollOffsets)
       return this.withinIncludingScrolloffsets(element, x, y);
     this.xcomp = x;
     this.ycomp = y;
-    this.offset = this.cumulativeOffset(element);
+    this.offset = Element.cumulativeOffset(element);
 
     return (y >= this.offset[1] &&
             y <  this.offset[1] + element.offsetHeight &&
@@ -1636,11 +4067,11 @@
   },
 
   withinIncludingScrolloffsets: function(element, x, y) {
-    var offsetcache = this.realOffset(element);
+    var offsetcache = Element.cumulativeScrollOffset(element);
 
     this.xcomp = x + offsetcache[0] - this.deltaX;
     this.ycomp = y + offsetcache[1] - this.deltaY;
-    this.offset = this.cumulativeOffset(element);
+    this.offset = Element.cumulativeOffset(element);
 
     return (this.ycomp >= this.offset[1] &&
             this.ycomp <  this.offset[1] + element.offsetHeight &&
@@ -1659,132 +4090,104 @@
         element.offsetWidth;
   },
 
-  clone: function(source, target) {
-    source = $(source);
-    target = $(target);
-    target.style.position = 'absolute';
-    var offsets = this.cumulativeOffset(source);
-    target.style.top    = offsets[1] + 'px';
-    target.style.left   = offsets[0] + 'px';
-    target.style.width  = source.offsetWidth + 'px';
-    target.style.height = source.offsetHeight + 'px';
-  },
+  // Deprecation layer -- use newer Element methods now (1.5.2).
 
-  page: function(forElement) {
-    var valueT = 0, valueL = 0;
+  cumulativeOffset: Element.Methods.cumulativeOffset,
 
-    var element = forElement;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
+  positionedOffset: Element.Methods.positionedOffset,
 
-      // Safari fix
-      if (element.offsetParent==document.body)
-        if (Element.getStyle(element,'position')=='absolute') break;
+  absolutize: function(element) {
+    Position.prepare();
+    return Element.absolutize(element);
+  },
 
-    } while (element = element.offsetParent);
+  relativize: function(element) {
+    Position.prepare();
+    return Element.relativize(element);
+  },
 
-    element = forElement;
-    do {
-      valueT -= element.scrollTop  || 0;
-      valueL -= element.scrollLeft || 0;
-    } while (element = element.parentNode);
+  realOffset: Element.Methods.cumulativeScrollOffset,
 
-    return [valueL, valueT];
-  },
+  offsetParent: Element.Methods.getOffsetParent,
 
-  clone: function(source, target) {
-    var options = Object.extend({
-      setLeft:    true,
-      setTop:     true,
-      setWidth:   true,
-      setHeight:  true,
-      offsetTop:  0,
-      offsetLeft: 0
-    }, arguments[2] || {})
+  page: Element.Methods.viewportOffset,
 
-    // find page position of source
-    source = $(source);
-    var p = Position.page(source);
+  clone: function(source, target, options) {
+    options = options || { };
+    return Element.clonePosition(target, source, options);
+  }
+};
 
-    // find coordinate system to use
-    target = $(target);
-    var delta = [0, 0];
-    var parent = null;
-    // delta [0,0] will do fine with position: fixed elements,
-    // position:absolute needs offsetParent deltas
-    if (Element.getStyle(target,'position') == 'absolute') {
-      parent = Position.offsetParent(target);
-      delta = Position.page(parent);
-    }
+/*--------------------------------------------------------------------------*/
 
-    // correct by body offsets (fixes Safari)
-    if (parent == document.body) {
-      delta[0] -= document.body.offsetLeft;
-      delta[1] -= document.body.offsetTop;
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+  function iter(name) {
+    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
+  }
+
+  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+  function(element, className) {
+    className = className.toString().strip();
+    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+  } : function(element, className) {
+    className = className.toString().strip();
+    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+    if (!classNames && !className) return elements;
+
+    var nodes = $(element).getElementsByTagName('*');
+    className = ' ' + className + ' ';
+
+    for (var i = 0, child, cn; child = nodes[i]; i++) {
+      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+          (classNames && classNames.all(function(name) {
+            return !name.toString().blank() && cn.include(' ' + name + ' ');
+          }))))
+        elements.push(Element.extend(child));
     }
+    return elements;
+  };
 
-    // set position
-    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
-    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
-    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
-    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
-  },
+  return function(className, parentElement) {
+    return $(parentElement || document.body).getElementsByClassName(className);
+  };
+}(Element.Methods);
 
-  absolutize: function(element) {
-    element = $(element);
-    if (element.style.position == 'absolute') return;
-    Position.prepare();
+/*--------------------------------------------------------------------------*/
 
-    var offsets = Position.positionedOffset(element);
-    var top     = offsets[1];
-    var left    = offsets[0];
-    var width   = element.clientWidth;
-    var height  = element.clientHeight;
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
 
-    element._originalLeft   = left - parseFloat(element.style.left  || 0);
-    element._originalTop    = top  - parseFloat(element.style.top || 0);
-    element._originalWidth  = element.style.width;
-    element._originalHeight = element.style.height;
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
 
-    element.style.position = 'absolute';
-    element.style.top    = top + 'px';;
-    element.style.left   = left + 'px';;
-    element.style.width  = width + 'px';;
-    element.style.height = height + 'px';;
+  set: function(className) {
+    this.element.className = className;
   },
 
-  relativize: function(element) {
-    element = $(element);
-    if (element.style.position == 'relative') return;
-    Position.prepare();
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
 
-    element.style.position = 'relative';
-    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
-    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
 
-    element.style.top    = top + 'px';
-    element.style.left   = left + 'px';
-    element.style.height = element._originalHeight;
-    element.style.width  = element._originalWidth;
+  toString: function() {
+    return $A(this).join(' ');
   }
-}
+};
 
-// Safari returns margins on body which is incorrect if the child is absolutely
-// positioned.  For performance reasons, redefine Position.cumulativeOffset for
-// KHTML/WebKit only.
-if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
-  Position.cumulativeOffset = function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      if (element.offsetParent == document.body)
-        if (Element.getStyle(element, 'position') == 'absolute') break;
+Object.extend(Element.ClassNames.prototype, Enumerable);
 
-      element = element.offsetParent;
-    } while (element);
+/*--------------------------------------------------------------------------*/
 
-    return [valueL, valueT];
-  }
-}
+Element.addMethods();

Modified: jifty/branches/virtual-models/share/web/static/js/rico.js
==============================================================================
--- jifty/branches/virtual-models/share/web/static/js/rico.js	(original)
+++ jifty/branches/virtual-models/share/web/static/js/rico.js	Sat Dec  1 17:17:34 2007
@@ -32,7 +32,7 @@
 
 //-------------------- rico.js
 var Rico = {
-  Version: '1.1.2.jifty.r963',
+  Version: '1.1.2.jifty.r4563',
   prototypeVersion: parseFloat(Prototype.Version.split(".")[0] + "." + Prototype.Version.split(".")[1])
 }
 
@@ -41,13 +41,10 @@
 
 Rico.ArrayExtensions = new Array();
 
-if (Object.prototype.extend) {
-   Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Object.prototype.extend;
+if (Object.extend) {
+   Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Object.extend;
 }else{
-  Object.prototype.extend = function(object) {
-    return Object.extend.apply(this, [this, object]);
-  }
-  Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Object.prototype.extend;
+    throw("Rico requires Object.extend from Prototype");
 }
 
 if (Array.prototype.push) {

Modified: jifty/branches/virtual-models/share/web/static/js/scriptaculous/effects.js
==============================================================================
--- jifty/branches/virtual-models/share/web/static/js/scriptaculous/effects.js	(original)
+++ jifty/branches/virtual-models/share/web/static/js/scriptaculous/effects.js	Sat Dec  1 17:17:34 2007
@@ -231,10 +231,10 @@
   get: function(queueName) {
     if(typeof queueName != 'string') return queueName;
     
-    if(!this.instances[queueName])
-      this.instances[queueName] = new Effect.ScopedQueue();
+    if(!this.instances.get(queueName))
+      this.instances.set(queueName, new Effect.ScopedQueue());
       
-    return this.instances[queueName];
+    return this.instances.get(queueName);
   }
 }
 Effect.Queue = Effect.Queues.get('global');

Modified: jifty/branches/virtual-models/t/01-dependencies.t
==============================================================================
--- jifty/branches/virtual-models/t/01-dependencies.t	(original)
+++ jifty/branches/virtual-models/t/01-dependencies.t	Sat Dec  1 17:17:34 2007
@@ -21,7 +21,6 @@
     return unless -f $_;
     return if $File::Find::dir =~ m!/.svn($|/)!;
     return if $File::Find::name =~ /~$/;
-    return if $File::Find::name =~ /generate-changelog/;
     return if $File::Find::name =~ /\.(pod|html)$/;
     
     # read in the file from disk

Added: jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Model/Texture.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Model/Texture.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,14 @@
+use strict;
+use warnings;
+
+package TestApp::Plugin::AppPluginHasModels::Model::Texture;
+use base qw/ TestApp::Plugin::AppPluginHasModels::Plugin::MyAppPlugin::Model::Texture /;
+
+use Jifty::DBI::Schema;
+use TestApp::Plugin::AppPluginHasModels::Record schema {
+    # yeah, i know this is a lame schema...
+    column exemplar =>
+        references TestApp::Plugin::AppPluginHasModels::Model::Wallpaper;     
+};
+
+1;

Added: jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Model/Wallpaper.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Model/Wallpaper.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+
+package TestApp::Plugin::AppPluginHasModels::Model::Wallpaper;
+use Jifty::DBI::Schema;
+
+use TestApp::Plugin::AppPluginHasModels::Record schema {
+    column name =>
+        type is 'text';
+
+    column color =>
+        references TestApp::Plugin::AppPluginHasModels::Model::Color;
+
+    column texture =>
+        references TestApp::Plugin::AppPluginHasModels::Model::Texture;
+};
+
+1;

Modified: jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Plugin/MyAppPlugin/Model/Color.pm
==============================================================================
--- jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Plugin/MyAppPlugin/Model/Color.pm	(original)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Plugin/MyAppPlugin/Model/Color.pm	Sat Dec  1 17:17:34 2007
@@ -12,4 +12,6 @@
         type is 'text';
 };
 
+sub table { 'myappplugin_colors' }
+
 1;

Added: jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Plugin/MyAppPlugin/Model/Texture.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/lib/TestApp/Plugin/AppPluginHasModels/Plugin/MyAppPlugin/Model/Texture.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,12 @@
+use strict;
+use warnings;
+
+package TestApp::Plugin::AppPluginHasModels::Plugin::MyAppPlugin::Model::Texture;
+use Jifty::DBI::Schema;
+
+use TestApp::Plugin::AppPluginHasModels::Plugin::MyAppPlugin::Record schema {
+    column name =>
+        type is 'text';
+};
+
+1;

Added: jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/app-model-ref.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/app-model-ref.t	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,76 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+
+=head1 DESCRIPTION
+
+A basic test harness for the Wallpaper model.
+
+=cut
+
+use lib 't/lib';
+use Jifty::SubTest;
+use Jifty::Test tests => 20;
+
+# Make sure we can load the model
+use_ok('TestApp::Plugin::AppPluginHasModels::Model::Wallpaper');
+
+# Grab a system user
+my $system_user = TestApp::Plugin::AppPluginHasModels::CurrentUser->superuser;
+ok($system_user, "Found a system user");
+
+# Try a color create
+my $color = TestApp::Plugin::AppPluginHasModels::Model::Color->new(current_user => $system_user);
+my ($color_id) = $color->create( name => 'Blue' );
+ok($color_id, 'we have a color');
+
+# Try a texture create
+my $texture = TestApp::Plugin::AppPluginHasModels::Model::Texture->new(current_user => $system_user);
+my ($texture_id) = $texture->create( name => 'Rough' );
+ok($texture_id, 'we have a texture');
+
+# Try testing a create
+my $o = TestApp::Plugin::AppPluginHasModels::Model::Wallpaper->new(current_user => $system_user);
+my ($id) = $o->create(
+    name => 'Plaid',
+    color => $color,
+    texture => $texture,
+);
+ok($id, "Wallpaper create returned success");
+ok($o->id, "New Wallpaper has valid id set");
+is($o->id, $id, "Create returned the right id");
+is($o->name, 'Plaid', 'name is Plaid');
+is($o->color->name, 'Blue', 'color is Blue');
+is($o->texture->name, 'Rough', 'texture is Rough');
+
+# And another
+$o->create();
+ok($o->id, "Wallpaper create returned another value");
+isnt($o->id, $id, "And it is different from the previous one");
+
+# Searches in general
+my $collection = TestApp::Plugin::AppPluginHasModels::Model::WallpaperCollection->new(current_user => $system_user);
+$collection->unlimit;
+is($collection->count, 2, "Finds two records");
+
+# Searches in specific
+$collection->limit(column => 'id', value => $o->id);
+is($collection->count, 1, "Finds one record with specific id");
+
+# Delete one of them
+$o->delete;
+$collection->redo_search;
+is($collection->count, 0, "Deleted row is gone");
+
+# And the other one is still there
+$collection->unlimit;
+is($collection->count, 1, "Still one left");
+
+# Needed for the action tests...
+Jifty::Test->web;
+
+# Make sure actions are available
+isa_ok($collection->as_search_action, 'TestApp::Plugin::AppPluginHasModels::Action::SearchWallpaper');
+isa_ok($o->as_create_action, 'TestApp::Plugin::AppPluginHasModels::Action::CreateWallpaper');
+isa_ok($o->as_update_action, 'TestApp::Plugin::AppPluginHasModels::Action::UpdateWallpaper');
+isa_ok($o->as_delete_action, 'TestApp::Plugin::AppPluginHasModels::Action::DeleteWallpaper');

Added: jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/app-model.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/app-model.t	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,62 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+
+=head1 DESCRIPTION
+
+A basic test harness for the Color model.
+
+=cut
+
+use lib 't/lib';
+use Jifty::SubTest;
+use Jifty::Test tests => 16;
+
+# Make sure we can load the model
+use_ok('TestApp::Plugin::AppPluginHasModels::Model::Color');
+
+# Grab a system user
+my $system_user = TestApp::Plugin::AppPluginHasModels::CurrentUser->superuser;
+ok($system_user, "Found a system user");
+
+# Try testing a create
+my $o = TestApp::Plugin::AppPluginHasModels::Model::Color->new(current_user => $system_user);
+my ($id) = $o->create();
+ok($id, "Color create returned success");
+ok($o->id, "New Color has valid id set");
+is($o->id, $id, "Create returned the right id");
+
+# Does it use a prefixed table
+is($o->table, 'myappplugin_colors', 'custom plugin table name');
+
+# And another
+$o->create();
+ok($o->id, "Color create returned another value");
+isnt($o->id, $id, "And it is different from the previous one");
+
+# Searches in general
+my $collection = TestApp::Plugin::AppPluginHasModels::Model::ColorCollection->new(current_user => $system_user);
+$collection->unlimit;
+is($collection->count, 2, "Finds two records");
+
+# Searches in specific
+$collection->limit(column => 'id', value => $o->id);
+is($collection->count, 1, "Finds one record with specific id");
+
+# Delete one of them
+$o->delete;
+$collection->redo_search;
+is($collection->count, 0, "Deleted row is gone");
+
+# And the other one is still there
+$collection->unlimit;
+is($collection->count, 1, "Still one left");
+
+# Needed for the action tests...
+Jifty::Test->web;
+
+# Make sure actions are available
+isa_ok($collection->as_search_action, 'TestApp::Plugin::AppPluginHasModels::Action::SearchColor');
+isa_ok($o->as_create_action, 'TestApp::Plugin::AppPluginHasModels::Action::CreateColor');
+isa_ok($o->as_update_action, 'TestApp::Plugin::AppPluginHasModels::Action::UpdateColor');
+isa_ok($o->as_delete_action, 'TestApp::Plugin::AppPluginHasModels::Action::DeleteColor');

Modified: jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/plugin-model.t
==============================================================================
--- jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/plugin-model.t	(original)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-AppPluginHasModels/t/plugin-model.t	Sat Dec  1 17:17:34 2007
@@ -27,7 +27,7 @@
 is($o->id, $id, "Create returned the right id");
 
 # Does it use a prefixed table
-is($o->table, 'testapp_plugin_apppluginhasmodels_plugin_myappplugin_colors', 'plugin table prefix');
+is($o->table, 'myappplugin_colors', 'custom plugin table name');
 
 # And another
 $o->create();

Modified: jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/share/web/templates/content.html
==============================================================================
--- jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/share/web/templates/content.html	(original)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/share/web/templates/content.html	Sat Dec  1 17:17:34 2007
@@ -1 +1 @@
-content
+original content

Modified: jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/share/web/templates/onclick.html
==============================================================================
--- jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/share/web/templates/onclick.html	(original)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/share/web/templates/onclick.html	Sat Dec  1 17:17:34 2007
@@ -3,7 +3,7 @@
 args:<% Jifty->web->request->arguments->{'J:V-region-content'} || ''%>
 
 <% Jifty->web->link(
-               label   => 'content',
+               label   => 'replace content',
                id => 'replace_content',
                onclick => {
                    replace_with => '/content1.html',
@@ -17,4 +17,10 @@
 ) %>
 
 
+<a id="original_content" 
+href="/onclick.html?J:V-region-content=%2Fcontent.html"
+onclick="return update(
+{'continuation':{},'actions':{},'fragments':[{'mode':'Replace','args':{},'region':'content','path':'/content.html'}],'action_arguments':{}},
+this );">revert content</a>
+
 </&>

Modified: jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/t/onclick.t
==============================================================================
--- jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/t/onclick.t	(original)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OnClick/t/onclick.t	Sat Dec  1 17:17:34 2007
@@ -2,21 +2,33 @@
 use warnings;
 use lib 't/lib';
 use Jifty::SubTest;
-use Jifty::Test tests => 6;
+use Jifty::Test tests => 10;
 use Jifty::Test::WWW::Selenium;
 use utf8;
 
-my $server  = Jifty::Test->make_server;
-my $sel = Jifty::Test::WWW::Selenium->rc_ok( $server );
-my $URL = $server->started_ok;
+my $server = Jifty::Test->make_server;
+my $sel    = Jifty::Test::WWW::Selenium->rc_ok($server);
+my $URL    = $server->started_ok;
 
 $sel->open_ok("/onclick.html");
 $sel->click_ok("//a[\@id='replace_content']");
 
+sleep 2; # in case the click returning slowly
+
 my $html = $sel->get_html_source;
 
+
 like( $html, qr/yatta/, 'replace content correctly' );
 unlike( $html, qr{args:/content1\.html}, 'replaced by javascript' );
 
+$sel->click_ok("//a[\@id='original_content']");
+is( $sel->get_alert,
+    'please use Jifty.update instead of update.',
+    'bare update is deprecated'
+);
+$html = $sel->get_html_source;
+like( $html, qr/original content/, 'replace content correctly' );
+unlike( $html, qr{args:/content\.html}, 'replaced by javascript' );
+
 $sel->stop;
 

Added: jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/OtherThingy.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/OtherThingy.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,17 @@
+use strict;
+use warnings;
+
+package TestApp::Model::OtherThingy;
+use Jifty::DBI::Schema;
+
+use TestApp::Record schema {
+
+  column value => type is 'text',  is mandatory;
+  column user_id => refers_to TestApp::Model::User;
+
+};
+
+use Jifty::RightsFrom column => 'user_id';
+
+1;
+

Added: jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/Thingy.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/Thingy.pm	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,17 @@
+use strict;
+use warnings;
+
+package TestApp::Model::Thingy;
+use Jifty::DBI::Schema;
+
+use TestApp::Record schema {
+
+  column value => type is 'text',  is mandatory;
+  column user_id => refers_to TestApp::Model::User;
+
+};
+
+use Jifty::RightsFrom column => 'user';
+
+1;
+

Modified: jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/User.pm
==============================================================================
--- jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/User.pm	(original)
+++ jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/User.pm	Sat Dec  1 17:17:34 2007
@@ -50,5 +50,17 @@
     'America/Anchorage'
 }
 
+sub current_user_can {
+    my $self = shift;
+    my $right = shift;
+    my %args = @_;
+
+    return 1 if $self->SUPER::current_user_can($right => %args);
+    
+    return 1 if $self->current_user->id and $self->id and $self->current_user->id == $self->id;
+
+    return 0;
+}
+
 1;
 

Modified: jifty/branches/virtual-models/t/TestApp/t/11-current_user.t
==============================================================================
--- jifty/branches/virtual-models/t/TestApp/t/11-current_user.t	(original)
+++ jifty/branches/virtual-models/t/TestApp/t/11-current_user.t	Sat Dec  1 17:17:34 2007
@@ -23,6 +23,7 @@
 
 # Make it so that all users have full access
 TestApp::Model::User->add_trigger( before_access => sub { 'allow' } );
+TestApp::Model::User->finalize_triggers if TestApp::Model::User->can('finalize_triggers');
 
 # Create two users
 my $o = TestApp::Model::User->new(current_user => $system_user);

Added: jifty/branches/virtual-models/t/TestApp/t/19-rightsfrom.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/t/19-rightsfrom.t	Sat Dec  1 17:17:34 2007
@@ -0,0 +1,88 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+
+=head1 DESCRIPTION
+
+Test the RightsFrom mixin.
+
+=cut
+
+use lib 't/lib';
+use Jifty::SubTest;
+
+use Jifty::Test no_plan => 1;;
+
+use_ok('TestApp::Model::User');
+use_ok('TestApp::Model::Thingy');
+use_ok('TestApp::Model::OtherThingy');
+use_ok('TestApp::CurrentUser');
+
+# Get a system user
+my $system_user = TestApp::CurrentUser->superuser;
+ok($system_user, "Found a system user");
+
+# Create users
+my $one = TestApp::Model::User->new(current_user => $system_user);
+$one->create( name => 'A User', email => 'auser at example.com', 
+            password => 'secret', tasty => 0 );
+ok($one->id, "New user has valid id set");
+is($one->name, "A User", "Has the right name");
+my $two = TestApp::Model::User->new(current_user => $system_user);
+$two->create( name => 'Bob', email => 'bob at example.com', 
+            password => 'secret2', tasty => 0 );
+ok($two->id, "New user has valid id set");
+
+# Create a CurrentUser
+my $one_user = TestApp::CurrentUser->new( id => $one->id );
+ok($one_user->id, "Loaded the current user");
+is($one_user->id, $one->id, "Has the right id");
+is($one_user->user_object->id, $one->id, "User object is right");
+is($one_user->user_object->name, $one->name, "Name is consistent");
+
+my $two_by_one = TestApp::Model::User->new( current_user => $one_user );
+$two_by_one->load( $two->id );
+ok($two_by_one->id, "Has an id");
+is($two_by_one->id, $two->id, "Has the right id");
+ok(!$two_by_one->current_user_can("read"), "Can read the remote user");
+ok(!$two_by_one->name, "Can't read their name");
+
+# And a thingy and otherthingy, one from each user; thingy has
+# rights_from 'user', otherthingy has rights from 'user_id';
+for my $class (qw/TestApp::Model::Thingy TestApp::Model::OtherThingy/) {
+    my $mine = $class->new(current_user => $system_user);
+    $mine->create( user_id => $one->id, value => "Whee" );
+    ok( $mine->id, "New object has a valid id");
+    is( $mine->user_id, $one->id, "Has right user" );
+    my $theirs = $class->new(current_user => $system_user);
+    $theirs->create( user_id => $two->id, value => "Not whee" );
+    ok( $theirs->id, "New object has a valid id");
+    is( $theirs->user_id, $two->id, "Has right user" );
+
+    my $access = $class->new( current_user => $one_user );
+    $access->load( $mine->id );
+    ok( $access->id, "Object has an id" );
+    is( $access->id, $mine->id, "Has the right id" );
+    ok( $access->current_user_can("read"), "I can read it");
+    ok( $access->value, "Has a value" );
+    is( $access->value, "Whee", "Can read the value" );
+    isa_ok( $access->user, "TestApp::Model::User", "Has a user" );
+    ok( $access->user_id, "Can read the user_id" );
+    ok( $access->user->id, "Can read the user->id" );
+    is( $access->user->id, $one->id, "Has the right user" );
+
+    $access->load( $theirs->id );
+    ok( $access->id, "Object has an id" );
+    is( $access->id, $theirs->id, "Has the right id" );
+    ok( !$access->current_user_can("read"), "I can't read it");
+    ok( !$access->value, "Can't read the value" );
+    isa_ok( $access->user, "TestApp::Model::User", "Has a user" );
+    ok( !$access->user_id, "Can't read the user_id" );
+    TODO:
+    {
+        local $TODO = "ACLs should apply to object refs, but can't";
+        # Except the problem is that Jifty current_user_can's often
+        # call their object refs, which would cause recursion.
+        ok( !$access->user->id, "Can't read the user->id" );
+    }
+}

Modified: jifty/branches/virtual-models/t/TestApp/t/before_access.t
==============================================================================
--- jifty/branches/virtual-models/t/TestApp/t/before_access.t	(original)
+++ jifty/branches/virtual-models/t/TestApp/t/before_access.t	Sat Dec  1 17:17:34 2007
@@ -22,6 +22,7 @@
     }
     return 'ignore';
 });
+TestApp::Model::User->finalize_triggers if TestApp::Model::User->can('finalize_triggers');
 
 # Try creating non-bob, which will be denied
 my $o = TestApp::Model::User->new(current_user => $system_user);


More information about the Jifty-commit mailing list