[Jifty-commit] r4281 - in jifty/branches/virtual-models: . bin examples/ShrinkURL examples/ShrinkURL/bin examples/ShrinkURL/doc examples/ShrinkURL/etc examples/ShrinkURL/lib examples/ShrinkURL/lib/ShrinkURL examples/ShrinkURL/lib/ShrinkURL/Action examples/ShrinkURL/lib/ShrinkURL/Model examples/ShrinkURL/log examples/ShrinkURL/share examples/ShrinkURL/share/po examples/ShrinkURL/share/web examples/ShrinkURL/share/web/static examples/ShrinkURL/share/web/templates examples/ShrinkURL/t examples/ShrinkURL/var examples/ShrinkURL/var/mason lib/Jifty lib/Jifty/Filter lib/Jifty/Plugin lib/Jifty/Plugin/Authentication/Password/Action lib/Jifty/Plugin/CompressedCSSandJS lib/Jifty/Plugin/OAuth lib/Jifty/Plugin/OAuth/Action lib/Jifty/Plugin/SkeletonApp lib/Jifty/Script lib/Jifty/Server lib/Jifty/View/Declare lib/Jifty/Web lib/Jifty/Web/Form share/plugins/Jifty/Plugin/AutoReference/web/static/js share/web/static/js t/TestApp-Plugin-OAuth t/TestApp-Plugin-OAuth/bin t/TestApp-Plugin-OAuth/doc t/TestApp-Plugin-OAuth/etc t/TestApp-Plugin-OAuth/lib t/TestApp-Plugin-OAuth/lib/TestApp t/TestApp-Plugin-OAuth/lib/TestApp/Plugin t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Action t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Model t/TestApp-Plugin-OAuth/log t/TestApp-Plugin-OAuth/share t/TestApp-Plugin-OAuth/share/po t/TestApp-Plugin-OAuth/share/web t/TestApp-Plugin-OAuth/share/web/static t/TestApp-Plugin-OAuth/share/web/templates t/TestApp-Plugin-OAuth/t t/TestApp-Plugin-OAuth/var t/TestApp/etc t/TestApp/lib/TestApp t/TestApp/lib/TestApp/Action t/TestApp/lib/TestApp/Model t/TestApp/t t/TestApp/t/config t/lib/Jifty

jifty-commit at lists.jifty.org jifty-commit at lists.jifty.org
Sat Oct 20 16:01:22 EDT 2007


Author: sterling
Date: Sat Oct 20 16:01:19 2007
New Revision: 4281

Added:
   jifty/branches/virtual-models/examples/ShrinkURL/
   jifty/branches/virtual-models/examples/ShrinkURL/Makefile.PL
   jifty/branches/virtual-models/examples/ShrinkURL/bin/
   jifty/branches/virtual-models/examples/ShrinkURL/bin/jifty   (contents, props changed)
   jifty/branches/virtual-models/examples/ShrinkURL/doc/
   jifty/branches/virtual-models/examples/ShrinkURL/etc/
   jifty/branches/virtual-models/examples/ShrinkURL/etc/config.yml
   jifty/branches/virtual-models/examples/ShrinkURL/lib/
   jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/
   jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Action/
   jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Action/CreateShrunkenURL.pm
   jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Dispatcher.pm
   jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Model/
   jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Model/ShrunkenURL.pm
   jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/View.pm
   jifty/branches/virtual-models/examples/ShrinkURL/log/
   jifty/branches/virtual-models/examples/ShrinkURL/share/
   jifty/branches/virtual-models/examples/ShrinkURL/share/po/
   jifty/branches/virtual-models/examples/ShrinkURL/share/web/
   jifty/branches/virtual-models/examples/ShrinkURL/share/web/static/
   jifty/branches/virtual-models/examples/ShrinkURL/share/web/templates/
   jifty/branches/virtual-models/examples/ShrinkURL/t/
   jifty/branches/virtual-models/examples/ShrinkURL/var/
   jifty/branches/virtual-models/examples/ShrinkURL/var/mason/
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Action/
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Action/AuthorizeRequestToken.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Dispatcher.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/AccessToken.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/Consumer.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/RequestToken.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Token.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/View.pm
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/Makefile.PL
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/bin/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/bin/jifty   (contents, props changed)
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/doc/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/etc/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/etc/config.yml
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Action/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Dispatcher.pm
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Model/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Model/User.pm
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Test.pm
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/log/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/share/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/share/po/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/share/web/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/share/web/static/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/share/web/templates/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/00-test-setup.t
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/01-basic.t
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/02-request-token.t
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/03-authorize.t
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/id_rsa
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/id_rsa.pub
   jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/var/
   jifty/branches/virtual-models/t/TestApp/etc/
   jifty/branches/virtual-models/t/TestApp/etc/config.yml
   jifty/branches/virtual-models/t/TestApp/etc/site_config.yml
   jifty/branches/virtual-models/t/TestApp/lib/TestApp/Action/SayHi.pm
   jifty/branches/virtual-models/t/TestApp/t/config/
   jifty/branches/virtual-models/t/TestApp/t/config/01-basic.t
   jifty/branches/virtual-models/t/TestApp/t/config/02-individual.t
   jifty/branches/virtual-models/t/TestApp/t/config/02-individual.t-config.yml
   jifty/branches/virtual-models/t/TestApp/t/config/03-nosubtest.t
   jifty/branches/virtual-models/t/TestApp/t/config/test_config.yml
   jifty/branches/virtual-models/t/TestApp/t/test_config.yml
Modified:
   jifty/branches/virtual-models/   (props changed)
   jifty/branches/virtual-models/META.yml
   jifty/branches/virtual-models/Makefile.PL
   jifty/branches/virtual-models/bin/generate-changelog
   jifty/branches/virtual-models/lib/Jifty/Config.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/I18N.pm
   jifty/branches/virtual-models/lib/Jifty/Notification.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/CompressedCSSandJS.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/CompressedCSSandJS/Dispatcher.pm
   jifty/branches/virtual-models/lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm
   jifty/branches/virtual-models/lib/Jifty/Schema.pm
   jifty/branches/virtual-models/lib/Jifty/Script/Po.pm
   jifty/branches/virtual-models/lib/Jifty/Server/Prefork.pm
   jifty/branches/virtual-models/lib/Jifty/Test.pm
   jifty/branches/virtual-models/lib/Jifty/Util.pm
   jifty/branches/virtual-models/lib/Jifty/View/Declare/CRUD.pm
   jifty/branches/virtual-models/lib/Jifty/Web.pm
   jifty/branches/virtual-models/lib/Jifty/Web/Form/Clickable.pm
   jifty/branches/virtual-models/lib/Jifty/Web/Form/Element.pm
   jifty/branches/virtual-models/lib/Jifty/Web/Menu.pm
   jifty/branches/virtual-models/share/plugins/Jifty/Plugin/AutoReference/web/static/js/autoreference.js
   jifty/branches/virtual-models/share/web/static/js/jifty.js
   jifty/branches/virtual-models/t/TestApp/lib/TestApp/Model/User.pm
   jifty/branches/virtual-models/t/TestApp/lib/TestApp/View.pm
   jifty/branches/virtual-models/t/lib/Jifty/SubTest.pm

Log:
 r13461 at dynpc145:  andrew | 2007-10-20 15:00:36 -0500
 Merge down from trunk.
  r12943 at dynpc145:  andrew | 2007-10-05 10:33:16 -0500
   r12942 at riddle (orig r4212):  sterling | 2007-10-04 10:33:24 -0500
    r6447 at viper:  andrew | 2007-10-04 10:33:05 -0500
    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.
   
  
  r13459 at dynpc145:  andrew | 2007-10-20 14:17:30 -0500
   r13095 at dynpc145 (orig r4215):  audreyt | 2007-10-09 13:17:34 -0500
   * Fix Jifty::Server::Prefork's document so it mentions
     Net::Server::PreFork, instead of Net::Server::Prefork.
     (Yes this is very confusing.)
   r13096 at dynpc145 (orig r4216):  sartak | 2007-10-09 16:42:26 -0500
   
   r13097 at dynpc145 (orig r4217):  sartak | 2007-10-09 16:42:30 -0500
    r43559 at onn:  sartak | 2007-10-09 17:21:52 -0400
    Add Jifty::Util->is_app_root to determine whether a path looks enough like the app root
   
   r13098 at dynpc145 (orig r4218):  sartak | 2007-10-09 16:42:35 -0500
    r43560 at onn:  sartak | 2007-10-09 17:42:13 -0400
    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 :)
   
   r13099 at dynpc145 (orig r4219):  sartak | 2007-10-09 17:13:19 -0500
    r43565 at onn:  sartak | 2007-10-09 18:13:07 -0400
    Revert my weird revert/checkin..
   
   r13226 at dynpc145 (orig r4220):  sartak | 2007-10-09 18:40:34 -0500
    r43567 at onn:  sartak | 2007-10-09 19:40:28 -0400
    Test config loading is now complete code-wise :)
    And it has tests now too! Just need to doc it in manual or Jifty::Config
   
   r13227 at dynpc145 (orig r4221):  sartak | 2007-10-09 18:56:14 -0500
    r43569 at onn:  sartak | 2007-10-09 19:56:09 -0400
    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. :)
   
   r13228 at dynpc145 (orig r4222):  sunnavy | 2007-10-10 02:41:43 -0500
   don't warn if database doesn't exist when dropping database
   r13230 at dynpc145 (orig r4224):  sunnavy | 2007-10-10 09:58:47 -0500
   warnings should be to STDERR
   r13231 at dynpc145 (orig r4225):  audreyt | 2007-10-10 11:24:11 -0500
   * The JIFTY_SITE_CONFIG environment variable was silently ignored.
   r13232 at dynpc145 (orig r4226):  sunnavy | 2007-10-10 13:13:24 -0500
   no need to print STDERR, we can warn in $SIG{__WARN__}
   r13233 at dynpc145 (orig r4227):  alexmv | 2007-10-10 21:44:04 -0500
    r23483 at zoq-fot-pik:  chmrr | 2007-10-10 22:41:42 -0400
     * Mailboxes need to have unique names for parallel testing
   
   r13234 at dynpc145 (orig r4228):  alexmv | 2007-10-10 21:44:21 -0500
    r23484 at zoq-fot-pik:  chmrr | 2007-10-10 22:42:01 -0400
     * Just a perltidy
   
   r13235 at dynpc145 (orig r4229):  alexmv | 2007-10-10 21:44:40 -0500
    r23485 at zoq-fot-pik:  chmrr | 2007-10-10 22:43:00 -0400
     * Pg wants to connect to template1, but CREATE DATABASE foo TEMPLATE template0
   
   r13237 at dynpc145 (orig r4231):  falcone | 2007-10-11 14:10:39 -0500
    r25357 at ketch:  falcone | 2007-10-11 15:05:07 -0400
    * add a bug tag
   
   r13238 at dynpc145 (orig r4232):  falcone | 2007-10-11 14:10:58 -0500
    r25358 at ketch:  falcone | 2007-10-11 15:06:00 -0400
    * make format_entry return text rather than printing
    * use the returned message to generate the changelog rather than printing to STDOUT
   
   r13239 at dynpc145 (orig r4233):  falcone | 2007-10-11 14:27:26 -0500
    r25361 at ketch:  falcone | 2007-10-11 15:26:34 -0400
    * usage documentation
   
   r13240 at dynpc145 (orig r4234):  falcone | 2007-10-11 15:07:09 -0500
    r25363 at ketch:  falcone | 2007-10-11 16:05:54 -0400
    * talk about how to do iterative editing
   
   r13241 at dynpc145 (orig r4235):  falcone | 2007-10-11 15:07:49 -0500
    r25364 at ketch:  falcone | 2007-10-11 16:06:15 -0400
    * add an exclude command for logs you don't need to ship
   
   r13242 at dynpc145 (orig r4236):  falcone | 2007-10-11 15:08:02 -0500
    r25365 at ketch:  falcone | 2007-10-11 16:06:34 -0400
    * fix so we don't try to print to a bogus filehandle
   
   r13243 at dynpc145 (orig r4237):  falcone | 2007-10-11 15:18:32 -0500
    r25369 at ketch:  falcone | 2007-10-11 16:18:00 -0400
    * use existing t-discard tag
    * don't autoskip things, just group them together and you can
      delete them 
   
   r13244 at dynpc145 (orig r4238):  alexmv | 2007-10-11 22:39:44 -0500
    r23516 at zoq-fot-pik:  chmrr | 2007-10-11 23:39:14 -0400
     * We depend on the bugfix that File::Spec->rel2abs("/foo","/foo") eq "."
   
   r13245 at dynpc145 (orig r4239):  falcone | 2007-10-12 09:15:54 -0500
    r25389 at ketch:  falcone | 2007-10-12 10:15:06 -0400
    * remove doubled loop over log messages
    * explicitly loop over uncategorized first, then offer
      the user to review all the changes again
    * reformatting
   
   r13331 at dynpc145 (orig r4240):  clkao | 2007-10-15 06:53:15 -0500
   Add cdn option to CompressedCSSandJS plugin.
   r13332 at dynpc145 (orig r4241):  jesse | 2007-10-15 07:32:46 -0500
    r68130 at 70:  jesse | 2007-10-15 13:28:14 +0100
    * Updating the Jifty web menu component to respect classes added by users
   
   r13333 at dynpc145 (orig r4242):  jesse | 2007-10-15 07:33:06 -0500
    r68131 at 70:  jesse | 2007-10-15 13:29:26 +0100
    * Cache gzipped output from the compressedcssandjs plugin
   
   r13334 at dynpc145 (orig r4243):  clkao | 2007-10-16 06:07:17 -0500
   Don't require recipients in Jifty::Notificaiton be objects.
   r13336 at dynpc145 (orig r4245):  sartak | 2007-10-16 16:31:35 -0500
    r43759 at onn:  sartak | 2007-10-16 17:31:18 -0400
    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
   
   r13424 at dynpc145 (orig r4246):  jesse | 2007-10-17 09:31:10 -0500
    r68158 at pinglin:  jesse | 2007-10-17 15:29:38 +0100
    * Added the ability to craft a Jifty button with a "submit" link which submits hardcoded arguments.
   
   r13425 at dynpc145 (orig r4247):  jesse | 2007-10-17 10:46:16 -0500
    r68164 at pinglin:  jesse | 2007-10-17 16:34:30 +0100
    * Fixing long-broken support for the "JIFTY_FASTTEST" env variable
   
   r13426 at dynpc145 (orig r4248):  jesse | 2007-10-17 10:46:27 -0500
    r68165 at pinglin:  jesse | 2007-10-17 16:35:39 +0100
    added a bit more semantic markup to the crud view
   
   r13427 at dynpc145 (orig r4249):  jesse | 2007-10-17 10:46:35 -0500
    r68166 at pinglin:  jesse | 2007-10-17 16:45:22 +0100
    
    * working toward onclick => { submit => { action => $action,    
                                    arguments => { foo => 'value', bar => 'other value'} }}
    
    Jifty's js still needs help
   
   r13428 at dynpc145 (orig r4250):  clkao | 2007-10-17 12:04:25 -0500
   Make action_arguments effective from js update()
   r13429 at dynpc145 (orig r4251):  alexmv | 2007-10-17 12:14:10 -0500
    r23709 at zoq-fot-pik:  chmrr | 2007-10-17 13:13:45 -0400
     * Untabify
   
   r13430 at dynpc145 (orig r4252):  sartak | 2007-10-17 14:43:22 -0500
    r43816 at onn:  sartak | 2007-10-17 15:43:11 -0400
    Start adding an OAuth plugin
   
   r13431 at dynpc145 (orig r4253):  sartak | 2007-10-17 17:08:30 -0500
    r43829 at onn:  sartak | 2007-10-17 18:08:12 -0400
    Some more fleshing out of the OAuth plugin
   
   r13432 at dynpc145 (orig r4254):  sartak | 2007-10-18 14:36:32 -0500
    r43835 at onn:  sartak | 2007-10-18 15:36:22 -0400
    Many improvements, including some pages
   
   r13433 at dynpc145 (orig r4255):  sartak | 2007-10-18 15:57:03 -0500
    r43843 at onn:  sartak | 2007-10-18 16:56:56 -0400
    Include the http method (GET, POST, etc) in the debug message
   
   r13434 at dynpc145 (orig r4256):  sartak | 2007-10-18 16:13:32 -0500
    r43845 at onn:  sartak | 2007-10-18 17:13:26 -0400
    Turns out we can't (so easily) have the URLs be configurable. Not too much of a problem though
    Other fixes
   
   r13435 at dynpc145 (orig r4257):  sartak | 2007-10-18 18:20:24 -0500
    r43847 at onn:  sartak | 2007-10-18 19:20:16 -0400
    Start writing tests
    Change timestamp to (ugh) time_stamp, because timestamp is a reserved word in a few SQL apps
   
   r13436 at dynpc145 (orig r4258):  sartak | 2007-10-18 20:52:03 -0500
    r43849 at onn:  sartak | 2007-10-18 21:51:56 -0400
    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 :)
   
   r13437 at dynpc145 (orig r4259):  sartak | 2007-10-18 22:34:56 -0500
    r43851 at onn:  sartak | 2007-10-18 23:34:48 -0400
    The first few test scripts are now complete, and uncovered many bugs. Yay.
   
   r13438 at dynpc145 (orig r4260):  sartak | 2007-10-18 23:30:18 -0500
    r43853 at onn:  sartak | 2007-10-19 00:30:11 -0400
    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
   
   r13439 at dynpc145 (orig r4261):  sartak | 2007-10-19 10:50:33 -0500
    r43871 at onn:  sartak | 2007-10-19 11:50:24 -0400
    Require Net::OAuth 0.04 because 0.03 needs my patch
    Don't run tests if Net::OAuth is uninstalled
   
   r13440 at dynpc145 (orig r4262):  clkao | 2007-10-19 11:21:59 -0500
   * Delay i18n handle init until we have session.
   * SkeletonApp dispatcher rule to set language in session.
   
   r13441 at dynpc145 (orig r4263):  audreyt | 2007-10-19 12:48:09 -0500
   * 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
   r13442 at dynpc145 (orig r4264):  sartak | 2007-10-19 15:22:19 -0500
    r43888 at onn:  sartak | 2007-10-19 16:22:10 -0400
    Refactor timestamp and nonce out of tokens and into consumers
    Thanks to hannesty for discussing how it works
   
   r13444 at dynpc145 (orig r4266):  sartak | 2007-10-19 16:18:43 -0500
    r43894 at onn:  sartak | 2007-10-19 17:18:30 -0400
    Users can now authorize/deny request tokens
   
   r13445 at dynpc145 (orig r4267):  sartak | 2007-10-19 17:19:55 -0500
    r43905 at onn:  sartak | 2007-10-19 18:19:45 -0400
    Doc
   
   r13446 at dynpc145 (orig r4268):  sartak | 2007-10-19 22:26:41 -0500
    r43907 at onn:  sartak | 2007-10-19 23:26:32 -0400
    Another example app: ShrinkURL, which is basically (surprise!) tinyurl
   
   r13447 at dynpc145 (orig r4269):  sartak | 2007-10-19 22:37:59 -0500
    r43909 at onn:  sartak | 2007-10-19 23:37:52 -0400
    More comments
   
   r13450 at dynpc145 (orig r4272):  sartak | 2007-10-19 23:34:31 -0500
    r43915 at onn:  sartak | 2007-10-20 00:34:20 -0400
    A lot of POD for OAuth, and some tiny fixes elsewhere
   
   r13451 at dynpc145 (orig r4273):  sartak | 2007-10-19 23:44:16 -0500
    r43917 at onn:  sartak | 2007-10-20 00:44:07 -0400
    Somehow this file got duplicated. I might've done it. shrug
   
   r13452 at dynpc145 (orig r4274):  sartak | 2007-10-19 23:47:37 -0500
    r43919 at onn:  sartak | 2007-10-20 00:47:22 -0400
    Add Dispatcher, revert some changes that aren't ready (weird ClassLoader errors? shrug)
   
   r13453 at dynpc145 (orig r4275):  sartak | 2007-10-19 23:54:38 -0500
    r43921 at onn:  sartak | 2007-10-20 00:54:31 -0400
    Dispatcher and config fixes, start adding a new test file
   
   r13454 at dynpc145 (orig r4276):  clkao | 2007-10-20 03:19:40 -0500
   simplify ccjs plugin with static handler method call.
   r13455 at dynpc145 (orig r4277):  clkao | 2007-10-20 03:28:15 -0500
   oops, should look for js files under js.
   r13456 at dynpc145 (orig r4278):  clkao | 2007-10-20 04:01:36 -0500
   Jifty::I18N: provide available_languages method.
   r13457 at dynpc145 (orig r4279):  clkao | 2007-10-20 04:15:33 -0500
   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.
   
   r13458 at dynpc145 (orig r4280):  clkao | 2007-10-20 08:56:48 -0500
   Provide get_current language method in Jifty::I18N.
  
 


Modified: jifty/branches/virtual-models/META.yml
==============================================================================
--- jifty/branches/virtual-models/META.yml	(original)
+++ jifty/branches/virtual-models/META.yml	Sat Oct 20 16:01:19 2007
@@ -2,7 +2,7 @@
 build_requires: 
   ExtUtils::MakeMaker: 6.11
 distribution_type: module
-generated_by: Module::Install version 0.670
+generated_by: Module::Install version 0.67
 license: Perl
 meta-spec: 
   url: http://module-build.sourceforge.net/META-spec-v1.3.html
@@ -24,6 +24,7 @@
   Cache::FileCache: 0
   Chart::Base: 0
   Class::Accessor::Named: 0
+  Crypt::OpenSSL::RSA: 0
   DBD::SQLite: 0
   Devel::Cover: 0
   Devel::EvalContext: 0
@@ -31,6 +32,7 @@
   Devel::Events::Generator::Objects: 0
   Devel::Events::Handler::ObjectTracker: 0
   Devel::Size: 0
+  Digest::HMAC_SHA1: 0
   GD: 0
   GD::Graph: 0
   Image::Info: 0
@@ -38,6 +40,10 @@
   Module::CoreList: 0
   Module::Install::Admin: 0.50
   Module::Refresh: 0.09
+  Net::OAuth::AccessTokenRequest: 0.04
+  Net::OAuth::ProtectedResourceRequest: 0.04
+  Net::OAuth::Request: 0.04
+  Net::OAuth::RequestTokenRequest: 0.04
   Net::OpenID::Consumer: 0
   Net::Server::Fork: 0
   Net::Server::PreFork: 0
@@ -60,6 +66,7 @@
   Class::Container: 0
   Class::Data::Inheritable: 0
   Class::Trigger: 0.12
+  Clone: 0.27
   Compress::Zlib: 0
   Crypt::CBC: 0
   Crypt::Rijndael: 0
@@ -82,6 +89,7 @@
   File::Find::Rule: 0
   File::MMagic: 0
   File::ShareDir: 0.04
+  File::Spec: 3.14
   HTML::Entities: 0
   HTML::Lint: 0
   HTML::Mason: 1.3101
@@ -95,7 +103,7 @@
   IPC::PubSub: 0.23
   IPC::Run3: 0
   JSON::Syck: 0.15
-  Jifty::DBI: 0.42
+  Jifty::DBI: 0.44
   LWP::UserAgent: 0
   Locale::Maketext::Extract: 0.20
   Locale::Maketext::Lexicon: 0.60
@@ -120,6 +128,7 @@
   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

Modified: jifty/branches/virtual-models/Makefile.PL
==============================================================================
--- jifty/branches/virtual-models/Makefile.PL	(original)
+++ jifty/branches/virtual-models/Makefile.PL	Sat Oct 20 16:01:19 2007
@@ -35,6 +35,7 @@
 requires('File::Find::Rule');
 requires('File::MMagic');
 requires('File::ShareDir' => '0.04');
+requires('File::Spec' => '3.14');
 requires('HTML::Entities');
 requires('HTML::Lint');
 requires('HTML::Mason' => 1.3101);           # HTML::Mason::Exceptions HTML::Mason::FakeApache HTML::Mason::MethodMaker HTML::Mason::Request HTML::Mason::Utils
@@ -168,6 +169,16 @@
         recommends('Devel::Events::Generator::Objects'),
         recommends('Devel::Size'),
     ],
+    'OAuth Plugin' => [
+        -default => 0,
+        recommends('Net::OAuth::Request' => '0.04'),
+        recommends('Net::OAuth::RequestTokenRequest' => '0.04'),
+        recommends('Net::OAuth::AccessTokenRequest' => '0.04'),
+        recommends('Net::OAuth::ProtectedResourceRequest' => '0.04'),
+
+        recommends('Crypt::OpenSSL::RSA'),
+        recommends('Digest::HMAC_SHA1'),
+    ],
 );
 
 

Modified: jifty/branches/virtual-models/bin/generate-changelog
==============================================================================
--- jifty/branches/virtual-models/bin/generate-changelog	(original)
+++ jifty/branches/virtual-models/bin/generate-changelog	Sat Oct 20 16:01:19 2007
@@ -1,4 +1,18 @@
 #perl -MFile::Slurp -MXML::Simple -MData::Dumper -e'print scalar Dumper ( XMLin( read_file(shift @ARGV).""))' 
+# Usage
+# --edit expects the output of svn log --xml as the first argument, and an intermediate
+#        file where we will be storing munged up xml that trags your tags and edits
+# --generate expects the intermediate file as the first argument and a plaintext
+#            changelog file as the second argument.  This changelog file is clobbered
+#
+# svn log --limit=100 --xml >> changelog.xml
+# generate-changelog --edit changelog.xml changelog.interim.xml
+# generate-changelog --generate changelog.interim.xml Changelog
+#
+# if you wish to do iterative editing, you can do
+# cp changelog.xml changelog.xml.orig
+# mv changelog.interim.xml changelog.xml
+# generate-changelog --edit changelog.xml changelog.interim.xml
 
 use warnings;
 use strict;
@@ -10,7 +24,7 @@
 use Term::ReadKey;
 use Data::Dumper;
          use File::Temp qw/ tempfile tempdir /;
-my @tags = qw(doc install core plugin security view t-discard backward-compatiblity-problem u-pubsub r-crud e-testing);
+my @tags = qw(doc install core plugin security view f-bug t-discard backward-compatiblity-problem u-pubsub r-crud e-testing);
 
 my %tags = map { substr($_,0,1) => $_ } @tags;
 my $mode = shift @ARGV;
@@ -34,17 +48,29 @@
 
 if ($mode eq '--edit') {
 
-foreach my $entry (@{$data->{'logentry'}}) {
-   my %entries; 
+    my $count;
+    my $total;
+    my %entries; 
     foreach my $entry (@{$data->{'logentry'}}) {
+        $total++;
         push @{$entries{$entry->{section}||'uncategorized'}}, $entry;
     }
+# iterate over the uncategorized ones first
+    my $uncat_total = @{$entries{uncategorized}};
+    foreach my $entry (@{$entries{uncategorized}}){ 
+        $count++;
+        act_on($entry, $count, $uncat_total);
+    }
+# all of them in case you want to frob stuff
+    print "All uncategorized changes complete, now iterating the full set\nHit any key to continue\n";
+    getchar();
+    $count = 0;
     foreach my $key ( keys %entries) {
         foreach my $entry (@{$entries{$key}}){ 
-    act_on($entry);
-    }
+            $count++;
+            act_on($entry, $count, $total);
+        }
     }
-}
 
     do_quit();
 } elsif ($mode eq '--generate') {
@@ -53,24 +79,30 @@
         push @{$entries{$entry->{section}||'uncategorized'}}, $entry;
     }
 
+    open (my $fh, ">$dest") or die "Can't open $dest for writing";
+
     foreach my $key ( keys %entries) {
     my $title = $key;
     $title =~ s/^\w\-//;
-        print uc($key)."\n";
-        print "=" x length($key) ;
-        print "\n\n";
+        print $fh uc($key)."\n";
+        print $fh "=" x length($key) ;
+        print $fh "\n\n";
 
         foreach my $entry (@{$entries{$key}}){ 
 
-        format_entry($entry) ;
-        print "\n";
+        print $fh format_entry($entry) ;
+        print $fh "\n";
         }
     }
 
+    close $fh;
+
 }
 
 sub act_on {
     my $entry = shift;
+    my $count = shift;
+    my $total = shift;
 
     my $console = Term::ANSIScreen->new;
     while (1) {
@@ -81,7 +113,8 @@
 
 
         if (!$entry->{'edited_msg'} && ref($entry->{msg})) { $entry->{'edited_msg'} = Dumper($entry->{'msg'}); }
-        format_entry ($entry => 1);
+        print "change $count / $total\n";
+        print format_entry($entry => 1);
     
     
         my $in = getchar();
@@ -94,6 +127,7 @@
         elsif ($in eq 'e') { $command = 'edit'; }
         elsif ($in eq 'q') { $command = 'quit'; }
         elsif ($in eq 't') { $command = 'tag' }
+        elsif ($in eq 'x') { $command = 'exclude' }
         elsif ($in eq ' ') {
             return
         }
@@ -116,8 +150,9 @@
         system( ( $ENV{EDITOR} || 'vi' ), $filename );
         $entry->{'edited_msg'} = read_file($filename);
     } elsif ($command eq 'quit') {
-
         do_quit();
+    } elsif ($command eq 'exclude') {
+        $entry->{section} = 't-discard';
     }
 
     }
@@ -157,38 +192,39 @@
 sub getchar {
 
     ReadMode 4;
-        my $key = ReadKey(0);
-            ReadMode 0;
+    my $key = ReadKey(0);
+    ReadMode 0;
 
     return $key
-            }
+}
 
 
 sub format_entry {
     my $entry = shift;
     my $verbose = shift ||0; 
-        if  ($verbose ) {
-      print "r".$entry->{revision}." - ";
-      print $entry->{'section'} || "UNCATEGORIZED - HIT 't'";   
-      print "\n".("="x60)."\n";
-      }
+    my $text = '';
+    if  ($verbose ) {
+        $text .= "r".$entry->{revision}." - ";
+        $text .= $entry->{'section'} || "UNCATEGORIZED - HIT 't'";   
+        $text .= "\n".("="x60)."\n";
+    }
 
-     my $msg = ( $entry->{'edited_msg'} || $entry->{'msg'});
+    my $msg = ( $entry->{'edited_msg'} || $entry->{'msg'});
 
     if ($msg =~ /^[\s\*]*\w/) {
         $msg =~ s/^[\s\*]*/ * /;
     }
 
-        $msg =~  s/\n+$//g;
-     $msg .= " - ".$entry->{'author'}."\n";
+    $msg =~  s/\n+$//g;
+    $msg .= " - ".$entry->{'author'}."\n";
 
 
      $msg =   autoformat ($msg,  { left=>0, right=>78 });
-        $msg =~  s/\n+$//g;
-      print $msg."\n";
-        if  ($verbose ) {
-
-        print YAML::Dump( $entry->{'paths'});
-        print "\n";
-    }
+     $msg =~  s/\n+$//g;
+     $text .= $msg."\n";
+     if  ($verbose ) {
+         $text .= YAML::Dump( $entry->{'paths'});
+         $text .= "\n";
+     }
+     return $text;
 }

Added: jifty/branches/virtual-models/examples/ShrinkURL/Makefile.PL
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/examples/ShrinkURL/Makefile.PL	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,8 @@
+use inc::Module::Install;
+
+name        'ShrinkURL';
+version     '0.01';
+requires    'Jifty' => '0.70824';
+requires    'Number::RecordLocator';
+
+WriteAll;

Added: jifty/branches/virtual-models/examples/ShrinkURL/bin/jifty
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/examples/ShrinkURL/bin/jifty	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+use File::Basename qw(dirname); 
+use UNIVERSAL::require;
+
+use Jifty;
+use Jifty::Script;
+
+local $SIG{INT} = sub { warn "Stopped\n"; exit; };
+Jifty::Script->dispatch();

Added: jifty/branches/virtual-models/examples/ShrinkURL/etc/config.yml
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/examples/ShrinkURL/etc/config.yml	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,68 @@
+--- 
+framework: 
+  SkipAccessControl: 1
+  AdminMode: 1
+  ApplicationClass: ShrinkURL
+  ApplicationName: ShrinkURL
+  ApplicationUUID: CA394F5C-7E9E-11DC-8297-EA4406D64C5E
+  ConfigFileVersion: 2
+  Database: 
+    CheckSchema: 1
+    Database: shrinkurl
+    Driver: SQLite
+    Host: localhost
+    Password: ''
+    RecordBaseClass: Jifty::DBI::Record::Cachable
+    User: ''
+    Version: 0.0.1
+  DevelMode: 1
+  L10N: 
+    PoDir: share/po
+  LogLevel: DEBUG
+  Mailer: Sendmail
+  MailerArgs: []
+
+  Plugins: 
+    - 
+      LetMe: {}
+
+    - 
+      SkeletonApp: {}
+
+    - 
+      REST: {}
+
+    - 
+      Halo: {}
+
+    - 
+      ErrorTemplates: {}
+
+    - 
+      OnlineDocs: {}
+
+    - 
+      CompressedCSSandJS: {}
+
+    - 
+      AdminUI: {}
+
+  PubSub: 
+    Backend: Memcached
+    Enable: ~
+  SkipAccessControl: 0
+  TemplateClass: ShrinkURL::View
+  Web: 
+    BaseURL: http://localhost
+    DataDir: var/mason
+    Globals: []
+
+    MasonConfig: 
+      autoflush: 0
+      default_escape_flags: h
+      error_format: text
+      error_mode: fatal
+    Port: 8888
+    ServeStaticFiles: 1
+    StaticRoot: share/web/static
+    TemplateRoot: share/web/templates

Added: jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Action/CreateShrunkenURL.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Action/CreateShrunkenURL.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,43 @@
+#!/usr/bin/env perl
+package ShrinkURL::Action::CreateShrunkenURL;
+use strict;
+use warnings;
+
+use base qw/Jifty::Action::Record::Create/;
+sub record_class { 'ShrinkURL::Model::ShrunkenURL' }
+
+# have we already shrunk this URL? if so, no need to do it again!
+sub take_action {
+    my $self = shift;
+    my $url = $self->argument_value('url');
+
+    my $shrunkenurl = ShrinkURL::Model::ShrunkenURL->new;
+    $shrunkenurl->load_by_cols(url => $url);
+
+    if ($shrunkenurl->id) {
+
+        # for the benefit of report_success
+        $self->record($shrunkenurl);
+
+        # for the benefit of the template that displays new shrunken URLs
+        # this is called in a superclass which we bypass
+        $self->result->content(id => $shrunkenurl->id);
+
+        # this too is called in a superclass
+        $self->report_success;
+
+        # Create actions return object's ID
+        return $shrunkenurl->id;
+    }
+
+    return $self->SUPER::take_action(@_);
+}
+
+# display a nice little message for the user
+sub report_success {
+    my $self = shift;
+    $self->result->message(_("URL shrunked to %1", $self->record->shrunken));
+}
+
+1;
+

Added: jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Dispatcher.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Dispatcher.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,26 @@
+#!/usr/bin/env perl
+package ShrinkURL::Dispatcher;
+use strict;
+use warnings;
+use Jifty::Dispatcher -base;
+
+# visiting / will let users create new shrunken URLs
+on '/' => show 'shrink';
+
+# any other URL (that has no path separator) is potentially a shrunken URL
+on '*' => run {
+    my $url = $1;
+
+    my $shrunkenurl = ShrinkURL::Model::ShrunkenURL->new;
+    $shrunkenurl->load_by_shrunken($url);
+
+    if ($shrunkenurl->id) {
+        redirect($shrunkenurl->url);
+    }
+
+    # if there's no valid URL, just let the person create a new one :)
+    redirect('/');
+};
+
+1;
+

Added: jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Model/ShrunkenURL.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/Model/ShrunkenURL.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,43 @@
+#!/usr/bin/env perl
+package ShrinkURL::Model::ShrunkenURL;
+use strict;
+use warnings;
+use Number::RecordLocator;
+my $generator = Number::RecordLocator->new;
+
+use Jifty::DBI::Schema;
+use Jifty::Record schema {
+    column url =>
+        is distinct,
+        is varchar(1000);
+};
+
+# shrunken URL is just an encoding of ID
+sub shrunken {
+    my $self = shift;
+    Jifty->web->url(path => $generator->encode($self->id));
+}
+
+# helper function so we can easily change the internal representation of
+# shrunken URLs if we desire
+sub load_by_shrunken {
+    my $self = shift;
+    my $shrunken = shift;
+    my $id = $generator->decode($shrunken);
+
+    return $self->load($id);
+}
+
+# prepend http:// if the scheme is not already there
+sub canonicalize_url {
+    my $self = shift;
+    my $url = shift;
+
+    $url = "http://$url"
+        unless $url =~ m{^\w+://};
+
+    return $url;
+}
+
+1;
+

Added: jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/View.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/examples/ShrinkURL/lib/ShrinkURL/View.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,60 @@
+#!/usr/bin/env perl
+package ShrinkURL::View;
+use strict;
+use warnings;
+use Jifty::View::Declare -base;
+
+template 'shrink' => page {
+
+    # render the "shrink a URL" widget, which we can put on any page of the app
+    render_region(
+        name => 'new_shrink',
+        path => '/misc/new_shrink',
+    );
+
+    # render an empty region that we push results onto
+    render_region(
+        name => 'new_shrinks',
+    );
+};
+
+template '/misc/new_shrink' => sub {
+    my $action = new_action(class => 'CreateShrunkenURL');
+    form {
+        Jifty->web->form->register_action($action);
+        render_action($action => ['url']);
+
+        form_submit(
+            submit  => $action,
+            label   => _('Shrink it!'),
+
+            onclick => [
+                { submit => $action },
+                {
+                    # prepend this result onto the empty region above
+                    region => 'new_shrinks',
+                    prepend => '/misc/shrunk_region',
+                    args => {
+                        id => { result_of => $action, name => 'id' },
+                    },
+                },
+            ],
+        );
+    };
+};
+
+template '/misc/shrunk_region' => sub {
+    my $id = get 'id';
+    my $shrunken = ShrinkURL::Model::ShrunkenURL->new;
+    $shrunken->load($id);
+
+    if ($shrunken->id) {
+        div {
+            strong { a { attr { href => $shrunken->shrunken } $shrunken->shrunken  } };
+            outs _(" is now a shortcut for %1.", $shrunken->url);
+        }
+    }
+};
+
+1;
+

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 Oct 20 16:01:19 2007
@@ -96,6 +96,9 @@
 the framework's C<TestConfig> or the C<JIFTY_TEST_CONFIG> environment
 variable.
 
+Note that the test config may be drawn from several files if you use
+L<Jifty::Test>. See the documentation of L<Jifty::Test::load_test_configs>.
+
 Values in the test configuration will clobber the site configuration.
 Values in the site configuration file clobber those in the vendor
 configuration file. Values in the vendor configuration file clobber
@@ -146,6 +149,9 @@
     # Load the site configuration file
     my $site = $self->load_file(
         Jifty::Util->absolute_path(
+            # Note: $ENV{'JIFTY_SITE_CONFIG'} is already considered
+            #       in ->_default_config_files(), but we || here again
+            #       in case someone overrided _default_config_files().
             $self->framework('SiteConfig') || $ENV{'JIFTY_SITE_CONFIG'}
         )
     );
@@ -222,7 +228,9 @@
     my $self = shift;
     my $config  = {
         framework => {
-            SiteConfig => 'etc/site_config.yml'
+            SiteConfig => (
+                $ENV{JIFTY_SITE_CONFIG} || 'etc/site_config.yml'
+            )
         }
     };
     return $self->_expand_relative_paths($config);

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 Oct 20 16:01:19 2007
@@ -48,6 +48,10 @@
         $args{$_} = $$value_ref->$_ if(defined($$value_ref->$_));
     }
 
+    # the floating timezone indicates a date, so we don't want to set any
+    # other timezone on it
+    $args{time_zone} = 'floating' if $$value_ref->time_zone =~ /floating/i;
+
     my $dt = Jifty::DateTime->new(%args);
 
     $$value_ref = $dt;

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 Oct 20 16:01:19 2007
@@ -191,9 +191,10 @@
     my $mode = shift || 'execute';
     my $database = $self->canonical_database_name;
     my $driver   = Jifty->config->framework('Database')->{'Driver'};
-    my $query = "CREATE DATABASE $database;\n";
+    my $query = "CREATE DATABASE $database";
+    $query .= " TEMPLATE template0" if $driver =~ /Pg/;
     if ( $mode eq 'print') {
-        print $query;
+        print "$query;\n";
     } elsif ( $driver !~ /SQLite/ ) {
         $self->simple_query($query);
     }
@@ -221,6 +222,8 @@
         $self->disconnect if $^O eq 'MSWin32';
         unlink($database);
     } else {
+        local $SIG{__WARN__} =
+          sub { warn $_[0] unless $_[0] =~ /exist|couldn't execute/i };
         $self->simple_query("DROP DATABASE $database");
     }
 }

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 Oct 20 16:01:19 2007
@@ -208,20 +208,21 @@
             Jifty::I18N->refresh;
         }
 
-        Jifty::I18N->get_language_handle;
-
         $self->cgi( $args{cgi} );
         $self->apache( HTML::Mason::FakeApache->new( cgi => $self->cgi ) );
 
         Jifty->web->request( Jifty::Request->new()->fill( $self->cgi ) );
         Jifty->web->response( Jifty::Response->new );
+
         Jifty->api->reset;
         for ( Jifty->plugins ) {
             $_->new_request;
         }
-        Jifty->log->debug( "Received request for " . Jifty->web->request->path );
+        Jifty->log->debug( "Received " . $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;

Modified: jifty/branches/virtual-models/lib/Jifty/I18N.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/I18N.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/I18N.pm	Sat Oct 20 16:01:19 2007
@@ -71,6 +71,25 @@
     my $lang = Jifty->config->framework('L10N')->{'Lang'};
     $lang = [defined $lang ? $lang : ()] unless ref($lang) eq 'ARRAY';
 
+    # Allow hard-coded allowed-languages in the config file
+    my $allowed_lang = Jifty->config->framework('L10N')->{'AllowedLang'};
+    $allowed_lang = [defined $allowed_lang ? $allowed_lang : ()] unless ref($allowed_lang) eq 'ARRAY';
+
+    if (@$allowed_lang) {
+        my $allowed_regex = join '|', map {
+            my $it = $_;
+            $it =~ tr<-A-Z><_a-z>; # lc, and turn - to _
+            $it =~ tr<_a-z0-9><>cd;  # remove all but a-z0-9_
+            $it;
+        } @$allowed_lang;
+
+        foreach my $lang ($self->available_languages) {
+            # "AllowedLang: zh" should let both zh_tw and zh_cn survive,
+            # so we just check ^ but not $.
+            $lang =~ /^$allowed_regex/ or delete $Jifty::I18N::{$lang.'::'};
+        }
+    }
+
     my $lh = $class->get_handle(@$lang);
 
     $DynamicLH = \$lh unless @$lang; 
@@ -106,6 +125,16 @@
     return $self;
 }
 
+=head2 available_languages
+
+Return an array of available languages
+
+=cut
+
+sub available_languages {
+    return map { /^(\w+)::/ ? $1 : () } sort keys %Jifty::I18N::;
+}
+
 =head2 _get_file_patterns
 
 Get list of patterns for all PO files in the project.
@@ -138,8 +167,17 @@
 =cut
 
 sub get_language_handle {
+    # XXX: subrequest should not need to get_handle again.
     my $self = shift;
-    $$DynamicLH = $self->get_handle() if $DynamicLH;
+    my $lang = Jifty->web->session->get('jifty_lang');
+    $$DynamicLH = $self->get_handle($lang ? $lang : ()) if $DynamicLH;
+}
+
+sub get_current_language {
+    return unless $DynamicLH;
+
+    my ($lang) = ref($$DynamicLH) =~ m/::(\w+)$/;
+    return $lang;
 }
 
 =head2 refresh

Modified: jifty/branches/virtual-models/lib/Jifty/Notification.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Notification.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Notification.pm	Sat Oct 20 16:01:19 2007
@@ -94,7 +94,7 @@
     my $self       = shift;
     my @recipients = $self->recipients;
     my $to         = join( ', ',
-        map { ( $_->can('email') ? $_->email : $_ ) } grep {$_} @recipients );
+        map { ( ref $_ && $_->can('email') ? $_->email : $_ ) } grep {$_} @recipients );
     $self->log->debug("Sending a ".ref($self)." to $to"); 
     return unless ($to);
     my $message = "";

Modified: jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/Authentication/Password/Action/SendPasswordReminder.pm	Sat Oct 20 16:01:19 2007
@@ -97,3 +97,4 @@
 }
 
 1;
+

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 Oct 20 16:01:19 2007
@@ -21,6 +21,7 @@
         js: 1
         css: 1
         jsmin: /path/to/jsmin
+        cdn: 'http://yourcdn.for.static.prefix/'
 
 =head1 DESCRIPTION
 
@@ -37,7 +38,7 @@
 
 =cut
 
-__PACKAGE__->mk_accessors(qw(css js jsmin cached_javascript cached_javascript_digest cached_javascript_time ));
+__PACKAGE__->mk_accessors(qw(css js jsmin cached_javascript cached_javascript_digest cached_javascript_time cdn ));
 
 =head2 init
 
@@ -55,6 +56,7 @@
     $self->css( $opt{css} );
     $self->js( $opt{js} );
     $self->jsmin( $opt{jsmin} );
+    $self->cdn( $opt{cdn} || '');
 
     Jifty::Web->add_trigger(
         name      => 'include_javascript',
@@ -89,7 +91,7 @@
     my $self = shift;
 
     $self->_generate_javascript;
-    Jifty->web->out( qq[<script type="text/javascript" src="/__jifty/js/]
+    Jifty->web->out( qq[<script type="text/javascript" src="@{[ $self->cdn ]}/__jifty/js/]
             . $self->cached_javascript_digest
             . qq[.js"></script>] );
     return 0;
@@ -109,23 +111,12 @@
         or Jifty->config->framework('DevelMode') ) {
         Jifty->log->debug("Generating JS...");
 
-        my @roots = map { Jifty::Util->absolute_path( File::Spec->catdir( $_, 'js' ) ) }
-                        Jifty->handler->view('Jifty::View::Static::Handler')->roots;
-
+        # for the file cascading logic
+        my $static_handler = Jifty->handler->view('Jifty::View::Static::Handler');
         my $js = "";
 
         for my $file ( @{ Jifty::Web->javascript_libs } ) {
-            my $include;
-
-            for my $root (@roots) {
-                my @spec = File::Spec->splitpath( $root, 1 );
-                my $path = File::Spec->catpath( @spec[ 0, 1 ], $file );
-
-                if ( -e $path ) {
-                    $include = $path;
-                    last;
-                }
-            }
+            my $include = $static_handler->file_path( File::Spec->catdir( 'js', $file ) );
 
             if ( defined $include ) {
                 my $fh;

Modified: jifty/branches/virtual-models/lib/Jifty/Plugin/CompressedCSSandJS/Dispatcher.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Plugin/CompressedCSSandJS/Dispatcher.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/CompressedCSSandJS/Dispatcher.pm	Sat Oct 20 16:01:19 2007
@@ -18,6 +18,9 @@
 
 use Jifty::Dispatcher -base;
 
+our($GZIP_CSS,$GZIP_JS);
+
+
 on '/__jifty/js/*' => run {
     my $arg = $1;
     if ( $arg !~ /^[0-9a-f]{32}\.js$/ ) {
@@ -52,7 +55,10 @@
         Jifty->handler->apache->header_out( "Content-Encoding" => "gzip" );
         Jifty->handler->apache->send_http_header();
         binmode STDOUT;
-        print Compress::Zlib::memGzip( $ccjs->cached_javascript );
+
+
+        print $GZIP_JS ||= Compress::Zlib::memGzip( $ccjs->cached_javascript );
+
     } else {
         Jifty->log->debug("Sending squished JS");
         Jifty->handler->apache->send_http_header();
@@ -91,7 +97,7 @@
         Jifty->handler->apache->header_out( "Content-Encoding" => "gzip" );
         Jifty->handler->apache->send_http_header();
         binmode STDOUT;
-        print Compress::Zlib::memGzip( Jifty->web->cached_css );
+        print $GZIP_CSS ||= Compress::Zlib::memGzip( Jifty->web->cached_css);
     } else {
         Jifty->log->debug("Sending squished CSS");
         Jifty->handler->apache->send_http_header();

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,158 @@
+package Jifty::Plugin::OAuth;
+use strict;
+use warnings;
+
+use base qw/Jifty::Plugin/;
+
+our $VERSION = 0.01;
+
+=head1 NAME
+
+Jifty::Plugin::OAuth
+
+=head1 DESCRIPTION
+
+A OAuth web services API for your Jifty app.
+
+=head1 WARNING
+
+This plugin is not yet complete. DO NOT USE IT.
+
+=head1 USAGE
+
+Add the following to your site_config.yml
+
+ framework:
+   Plugins:
+     - OAuth: {}
+
+=head1 GLOSSARY
+
+=over 4
+
+=item service provider
+
+A service provider is an application that has users who have private data. This
+plugin enables your Jifty application to be an OAuth service provider.
+
+=item consumer
+
+A consumer is an application that wants to access users' private data. The
+service provider (in this case, this plugin) ensures that this happens securely
+and with users' full approval. Without OAuth (or similar systems), this would
+be accomplished perhaps by the user giving the consumer her login information.
+Obviously not ideal.
+
+This plugin does not yet implement the consumer half of the protocol.
+
+=item request token
+
+A request token is a unique, random string that a user may authorize for a
+consumer.
+
+=item access token
+
+An access token is a unique, random string that a consumer can use to access
+private resources on the authorizing user's behalf. Consumers may only
+receive an access token if they have an authorized request token.
+
+=back
+
+=head1 NOTES
+
+You must provide public access to C</oauth/request_token> and
+C</oauth/access_token>.
+
+You must not allow public access to C</oauth/authorize>. C</oauth/authorize>
+depends on having the user be logged in.
+
+There is currently no way for consumers to add themselves. This might change in
+the future, but it would be a nondefault configuration. Consumers must
+contact you and provide you with the following data:
+
+=over 4
+
+=item consumer_key
+
+An arbitrary string that uniquely identifies a consumer. Preferably something
+random over, say, "Hiveminder".
+
+=item secret
+
+A (preferably random) string that is used to ensure that it's really the
+consumer you're talking to. After the consumer provides this to you, it's never
+sent in plaintext. It is always, however, included in cryptographic signatures.
+
+=item name
+
+A readable name to use in displaying the consumer to users. This is where you'd
+put "Hiveminder".
+
+=item url (optional)
+
+The website of the consumer.
+
+=item rsa_key (optional)
+
+The consumer's public RSA key. This is optional. Without it, they will not be
+able to use the RSA-SHA1 signature method. They can still use HMAC-SHA1 though.
+
+=back
+
+=head1 TECHNICAL DETAILS
+
+OAuth is an open protocol that enables consumers to access users' private data
+in a secure and authorized manner. The way it works is:
+
+=over 4
+
+=item
+
+The consumer establishes a key and a secret with the service provider. This
+step only happens once.
+
+=item
+
+The user is using the consumer's application and decides that she wants to
+use some data that she already has on the service provider's application.
+
+=item
+
+The consumer asks the service provider for a request token. The service
+provider generates one and gives it to the consumer.
+
+=item
+
+The consumer directs the user to the service provider with that request token.
+
+=item
+
+The user logs in and authorizes that request token.
+
+=item
+
+The service provider directs the user back to the consumer.
+
+=item
+
+The consumer asks the service provider to exchange his authorized request token
+for an access token. This access token lets the consumer access resources on
+the user's behalf in a limited way, for a limited amount of time.
+
+=back
+
+By establishing secrets and using signatures and timestamps, this can be done
+in a very secure manner. For example, a replay attack (an eavesdropper repeats
+a request made by a legitimate consumer) is actively defended against.
+
+=head1 SEE ALSO
+
+L<Net::OAuth::Request>, L<http://oauth.net/>
+
+=head1 AUTHOR
+
+Shawn M Moore C<< <sartak at bestpractical.com> >>
+
+=cut
+
+1;

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Action/AuthorizeRequestToken.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Action/AuthorizeRequestToken.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,74 @@
+package Jifty::Plugin::OAuth::Action::AuthorizeRequestToken;
+use warnings;
+use strict;
+use base qw/Jifty::Action/;
+
+=head1 NAME
+
+Jifty::Plugin::OAuth::Action::AuthorizeRequestToken
+
+=cut
+
+use Jifty::Param::Schema;
+use Jifty::Action schema {
+
+    param 'token',
+        render as 'text',
+        max_length is 30,
+        hints are 'The site you just came from should have provided it',
+        ajax validates;
+
+    param 'authorize',
+        valid_values are qw(allow deny);
+
+};
+
+=head2 validate_token
+
+Make sure we have such a token, and that it is not already authorized
+
+=cut
+
+sub validate_token {
+    my $self = shift;
+    my $token = shift;
+
+    my $request_token = Jifty::Plugin::OAuth::Model::RequestToken->new(current_user => Jifty::CurrentUser->superuser);
+    $request_token->load_by_cols(
+        token => $token,
+        authorized => 'f',
+    );
+
+    return $self->validation_error(token => "I don't know of that request token.") unless $request_token->id;
+
+    return $self->validation_ok('token');
+}
+
+=head2 take_action
+
+Actually authorize or deny this request token
+
+=cut
+
+sub take_action {
+    my $self = shift;
+
+    my $token = Jifty::Plugin::OAuth::Model::RequestToken->new(current_user => Jifty::CurrentUser->superuser);
+    $token->load_by_cols(
+        token => $self->argument_value('token'),
+    );
+
+    if ($self->argument_value('authorize') eq 'allow') {
+        $token->set_authorized('t');
+        $self->result->message("Allowing " . $token->consumer->name . " to access your stuff.");
+    }
+    else {
+        $token->delete;
+        $self->result->message("Denying " . $token->consumer->name . " the right to access your stuff.");
+    }
+
+    return 1;
+}
+
+1;
+

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Dispatcher.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Dispatcher.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,289 @@
+package Jifty::Plugin::OAuth::Dispatcher;
+use warnings;
+use strict;
+
+use Jifty::Dispatcher -base;
+
+use Net::OAuth::RequestTokenRequest;
+use Net::OAuth::AccessTokenRequest;
+use Net::OAuth::ProtectedResourceRequest;
+use Crypt::OpenSSL::RSA;
+
+on     POST '/oauth/request_token' => \&request_token;
+before GET  '/oauth/authorize'     => \&authorize;
+on     POST '/oauth/access_token'  => \&access_token;
+
+=head2 abortmsg CODE, MSG
+
+Helper function to abort with a debug message. Maybe should be factored into
+the C<abort> procedure?
+
+=cut
+
+sub abortmsg {
+    my ($code, $msg) = @_;
+    Jifty->log->debug($msg) if defined($msg);
+    abort($code || 400);
+}
+
+=head2 request_token
+
+The consumer wants a request token
+
+=cut
+
+sub request_token {
+    my @params = qw/consumer_key signature_method signature
+                    timestamp nonce version/;
+    set no_abort => 0;
+
+    my %oauth_params  = get_parameters(@params);
+    my $consumer      = get_consumer($oauth_params{consumer_key});
+    my $signature_key = get_signature_key($oauth_params{signature_method}, $consumer);
+    my ($ok, $msg) = $consumer->is_valid_request(@oauth_params{qw/timestamp nonce/});
+    abortmsg(401, $msg) if !$ok;
+
+    # Net::OAuth::Request will die hard if it doesn't get everything it wants
+    my $request = eval { Net::OAuth::RequestTokenRequest->new(
+        request_url     => Jifty->web->url(path => '/oauth/request_token'),
+        request_method  => Jifty->handler->apache->method(),
+        consumer_secret => $consumer->secret,
+        signature_key   => $signature_key,
+
+        map { $_ => $oauth_params{$_} } @params
+    ) };
+
+    abortmsg(400, "Unable to create RequestTokenRequest: $@") if $@ || !defined($request);
+
+    # make sure the signature matches the rest of what the consumer gave us
+    abortmsg(401, "Invalid signature (type: $oauth_params{signature_method}).") unless $request->verify;
+
+    # ok, everything checks out. send them back a request token
+    # at this point, the only things that could go wrong are:
+    # 1) we've already seen this nonce and timestamp. possibly a replay attack,
+    #    so we abort
+    # 2) we tried a bunch of times to create a unique token but failed. abort
+    #    because we don't have any other option
+
+    my $token = Jifty::Plugin::OAuth::Model::RequestToken->new(current_user => Jifty::CurrentUser->superuser);
+
+    ($ok, $msg) = eval {
+        $token->create(consumer => $consumer);
+    };
+
+    abortmsg(401, "Unable to create a Request Token: " . $@ || $msg)
+        if $@ || !$ok;
+
+    $consumer->made_request(@oauth_params{qw/timestamp nonce/});
+    set oauth_response => {
+        oauth_token        => $token->token,
+        oauth_token_secret => $token->secret
+    };
+    show 'oauth/response';
+}
+
+=head2 authorize
+
+The user is authorizing (or denying) a consumer's request token
+
+=cut
+
+sub authorize {
+    my @params = qw/token callback/;
+
+    set no_abort => 1;
+    my %oauth_params = get_parameters(@params);
+
+    set next => $oauth_params{callback};
+    set consumer => 'Some application';
+    del 'token';
+
+    if ($oauth_params{token}) {
+        my $request_token = Jifty::Plugin::OAuth::Model::RequestToken->new(current_user => Jifty::CurrentUser->superuser);
+        $request_token->load_by_cols(token => $oauth_params{token}, authorized => 'f');
+
+        if ($request_token->id) {
+            set consumer => $request_token->consumer;
+            set token    => $oauth_params{token};
+        }
+    }
+}
+
+=head2 access_token
+
+The consumer is trying to trade a request token for an access token
+
+=cut
+
+sub access_token {
+    my @params = qw/consumer_key signature_method signature
+                    timestamp nonce token version/;
+    set no_abort => 0;
+
+    my %oauth_params  = get_parameters(@params);
+    my $consumer      = get_consumer($oauth_params{consumer_key});
+    my $signature_key = get_signature_key($oauth_params{signature_method}, $consumer);
+    my ($ok, $msg) = $consumer->is_valid_request(@oauth_params{qw/timestamp nonce/});
+    abortmsg(401, $msg) if !$ok;
+
+    # is the request token they're using still valid?
+    my $request_token = Jifty::Plugin::OAuth::Model::RequestToken->new(current_user => Jifty::CurrentUser->superuser);
+    $request_token->load_by_cols(consumer => $consumer, token => $oauth_params{token});
+
+    abortmsg(401, "No token found for consumer ".$consumer->name." with key $oauth_params{token}") unless $request_token->id;
+
+    ($ok, $msg) = $request_token->can_trade_for_access_token;
+    abortmsg(401, "Cannot trade request token for access token: $msg") if !$ok;
+
+    # Net::OAuth::Request will die hard if it doesn't get everything it wants
+    my $request = eval { Net::OAuth::AccessTokenRequest->new(
+        request_url     => Jifty->web->url(path => '/oauth/access_token'),
+        request_method  => Jifty->handler->apache->method(),
+        consumer_secret => $consumer->secret,
+        token_secret    => $request_token->secret,
+        signature_key   => $signature_key,
+
+        map { $_ => $oauth_params{$_} } @params
+    ) };
+
+    abortmsg(400, "Unable to create AccessTokenRequest: $@") if $@ || !defined($request);
+
+    # make sure the signature matches the rest of what the consumer gave us
+    abortmsg(401, "Invalid signature (type: $oauth_params{signature_method}).") unless $request->verify;
+
+    my $token = Jifty::Plugin::OAuth::Model::AccessToken->new(current_user => Jifty::CurrentUser->superuser);
+
+    ($ok, $msg) = eval {
+        $token->create(consumer => $consumer,
+                       auth_as  => $request_token->authorized_by);
+    };
+
+    abortmsg(401, "Unable to create an Access Token: " . $@ || $msg)
+        if $@ || !defined($token) || !$ok;
+
+    $consumer->made_request(@oauth_params{qw/timestamp nonce/});
+    set oauth_response => {
+        oauth_token        => $token->token,
+        oauth_token_secret => $token->secret
+    };
+    show 'oauth/response';
+}
+
+=head2 get_consumer CONSUMER KEY
+
+Helper function to load a consumer by consumer key. Will abort if the key
+is unknown.
+
+=cut
+
+sub get_consumer {
+    my $key = shift;
+    my $consumer = Jifty::Plugin::OAuth::Model::Consumer->new(current_user => Jifty::CurrentUser->superuser);
+    $consumer->load_by_cols(consumer_key => $key);
+    abortmsg(401, "No known consumer with key $key") if !$consumer->id;
+    return $consumer;
+}
+
+=head2 get_signature_key SIGNATURE METHOD, CONSUMER
+
+Figures out the signature key for this consumer. Will abort if the signature
+method is unsupported, or if the consumer lacks the prerequisites for this
+signature method.
+
+Will return C<undef> is the signature key is consumer independent, as is the
+case for C<PLAINTEXT> and C<HMAC-SHA1>. C<RSA-SHA1> depends on the consumer
+having the C<rsa_key> field.
+
+=cut
+
+{
+    my %valid_signature_methods = map { $_ => 1 }
+                                  qw/PLAINTEXT HMAC-SHA1 RSA-SHA1/;
+    my %key_field = ('RSA-SHA1' => 'rsa_key');
+
+    sub get_signature_key {
+        my ($method, $consumer) = @_;
+        if (!$valid_signature_methods{$method}) {
+            abortmsg(400, "Unsupported signature method requested: $method");
+        }
+
+        my $field = $key_field{$method};
+
+        # this MUST return undef if the signature method requires no prior key
+        return undef if !defined($field);
+
+        my $key = $consumer->$field;
+
+        abortmsg(400, "Consumer does not have necessary field $field required for signature method $method")
+            unless defined $key;
+
+        if ($method eq 'RSA-SHA1') {
+            $key = Crypt::OpenSSL::RSA->new_public_key($key);
+        }
+
+        return $key;
+    }
+}
+
+=head2 get_parameters REQUIRED PARAMETERS
+
+This will retrieve all the request paremeters. This gets parameters besides
+the ones in the OAuth spec, because the signature is based on all such request
+parameters.
+
+Pass in by name all the OAuth-required parameters. Do not include the C<oauth_>
+prefix.
+
+The precedence of parameters, from highest priority to lowest priority, is:
+
+=over 4
+
+=item Authorization header
+
+=item WWW-Authenticate header
+
+=item POST parameters
+
+=item GET parameters (aka URL's query string)
+
+=back
+
+=cut
+
+sub get_parameters {
+    my %p;
+
+    # XXX: Check Authorization header
+    # XXX: Check WWW-Authenticate header
+
+    my %params = Jifty->handler->apache->params();
+    for (@_) {
+        $p{$_} = delete $params{"oauth_$_"}
+            if !defined $p{$_};
+    }
+
+    $p{version} ||= '1.0';
+
+    unless (get 'no_abort') {
+        # check to see if there are any unsupported parameters
+        while (my ($key, undef) = each %params) {
+            abortmsg(400, "Unsupported parameter: $key")
+                if $key =~ /^oauth_/;
+        }
+
+        # check to see if we're missing anything
+        for (@_) {
+            abortmsg(400, "Undefined required parameter: $_")
+                if !defined($p{$_});
+        }
+
+        if ($p{timestamp} && $p{timestamp} !~ /^\d+$/) {
+            abortmsg(400, "Malformed timestamp. Expected positive integer, got $p{timestamp}");
+        }
+    }
+
+    return %p;
+}
+
+1;
+

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/AccessToken.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/AccessToken.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,41 @@
+#!/usr/bin/env perl
+package Jifty::Plugin::OAuth::Model::AccessToken;
+use strict;
+use warnings;
+
+use base qw( Jifty::Plugin::OAuth::Token Jifty::Record );
+
+# kludge 1: you cannot call Jifty->app_class within schema {}
+my $app_user;
+BEGIN { $app_user = Jifty->app_class('Model', 'User') }
+
+use Jifty::DBI::Schema;
+use Jifty::Record schema {
+
+    # kludge 2: this kind of plugin cannot yet casually refer_to app models
+    column auth_as =>
+        type is 'integer';
+        #refers_to $app_user;
+
+    column valid_until =>
+        type is 'timestamp',
+        filters are 'Jifty::DBI::Filter::DateTime';
+
+    column secret =>
+        type is 'varchar';
+
+    column consumer =>
+        refers_to Jifty::Plugin::OAuth::Model::Consumer;
+
+};
+
+=head2 table
+
+AccessTokens are stored in the table C<oauth_access_tokens>.
+
+=cut
+
+sub table {'oauth_access_tokens'}
+
+1;
+

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/Consumer.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/Consumer.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,125 @@
+#!/usr/bin/env perl
+package Jifty::Plugin::OAuth::Model::Consumer;
+use strict;
+use warnings;
+
+use base qw( Jifty::Record );
+
+use Jifty::DBI::Schema;
+use Jifty::Record schema {
+
+    # the unique key that identifies a consumer
+    column consumer_key =>
+        type is 'varchar',
+        is distinct,
+        is required;
+
+    # a secret used in signing to verify that we have the real consumer (and
+    # not just someone who got ahold of the key)
+    column secret =>
+        type is 'varchar',
+        is required;
+
+    # the name of the consumer, e.g. Bob's Social Network
+    column name =>
+        type is 'varchar',
+        is required;
+
+    # the url of the consumer, e.g. http://social.bob/
+    column url =>
+        type is 'varchar';
+
+    column rsa_key =>
+        type is 'varchar',
+        hints are 'This is only necessary if you want to support RSA-SHA1 signatures';
+
+    # we use these to make sure we aren't being hit with a replay attack
+    column last_timestamp =>
+        type is 'integer',
+        is required,
+        default is 0;
+
+    column nonces =>
+        type is 'blob',
+        filters are 'Jifty::DBI::Filter::Storable';
+};
+
+=head2 table
+
+Consumers are stored in the table C<oauth_consumers>.
+
+=cut
+
+sub table {'oauth_consumers'}
+
+=head2 before_set_last_timestamp
+
+If the new timestamp is different from the last_timestamp, then clear any
+nonces we've used. Nonces must only be unique for requests of a given
+timestamp.
+
+Note that you should ALWAYS call is_valid_request before updating the
+last_timestamp. You should also verify the signature and make sure the request
+all went through before updating the last_timestamp. Otherwise an attacker
+may be able to create a request with an extraordinarily high timestamp and
+screw up the regular consumer.
+
+=cut
+
+sub before_set_last_timestamp {
+    my $self = shift;
+    my $new_ts = shift;
+
+    # uh oh, looks like sloppy coding..
+    if ($new_ts < $self->last_timestamp) {
+        die "The new timestamp is LESS than the last timestamp. You forgot to call is_valid_request!";
+    }
+
+    # if this is a new timestamp, then flush the nonces
+    if ($new_ts != $self->last_timestamp) {
+        $self->set_nonces( {} );
+    }
+}
+
+=head2 is_valid_request TIMESTAMP, NONCE
+
+This will do some sanity checks (as required for security by the OAuth spec).
+It will make sure that the timestamp is not less than the latest timestamp for
+this consumer. It will also make sure that the nonce hasn't been seen for
+this timestamp (very important).
+
+ALWAYS call this method when handling OAuth requests. EARLY.
+
+=cut
+
+sub is_valid_request {
+    my ($self, $timestamp, $nonce) = @_;
+    return (0, "Timestamp nonincreasing.")
+        if $timestamp < $self->last_timestamp;
+    return 1 if $timestamp > $self->last_timestamp;
+
+    # if this is the same timestamp as the last, we must check that the nonce
+    # is unique across the requests of these timestamps
+    return (0, "Already used this nonce.")
+        if defined $self->nonces->{$nonce};
+
+    return 1;
+}
+
+=head2 made_request TIMESTAMP, NONCE
+
+This method is to be called just before you're done processing an OAuth
+request. Parameters were valid, no errors occurred, everything's generally
+hunky-dory. This updates the C<last_timestamp> of the consumer, and sets the
+nonce as "used" for this new timestamp.
+
+=cut
+
+sub made_request {
+    my ($self, $timestamp, $nonce) = @_;
+    $self->set_last_timestamp($timestamp);
+    $self->nonces->{$nonce} = 1;
+}
+
+1;
+

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/RequestToken.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Model/RequestToken.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,95 @@
+#!/usr/bin/env perl
+package Jifty::Plugin::OAuth::Model::RequestToken;
+use strict;
+use warnings;
+
+use base qw( Jifty::Plugin::OAuth::Token Jifty::Record );
+
+# kludge 1: you cannot call Jifty->app_class within schema {}
+my $app_user;
+BEGIN { $app_user = Jifty->app_class('Model', 'User') }
+
+use Jifty::DBI::Schema;
+use Jifty::Record schema {
+
+    column valid_until =>
+        type is 'timestamp',
+        filters are 'Jifty::DBI::Filter::DateTime',
+        is required;
+
+    column authorized =>
+        type is 'boolean',
+        default is 'f';
+
+    # kludge 2: this kind of plugin cannot yet casually refer_to app models
+    column authorized_by =>
+        type is 'integer';
+        #refers_to $app_user;
+
+    column consumer =>
+        refers_to Jifty::Plugin::OAuth::Model::Consumer,
+        is required;
+
+    column used =>
+        type is 'boolean',
+        default is 'f';
+
+    column token =>
+        type is 'varchar',
+        is required;
+
+    column secret =>
+        type is 'varchar',
+        is required;
+
+};
+
+=head2 table
+
+RequestTokens are stored in the table C<oauth_request_tokens>.
+
+=cut
+
+sub table {'oauth_request_tokens'}
+
+=head2 after_set_authorized
+
+This will set the C<authorized_by> to the current user.
+
+=cut
+
+sub after_set_authorized {
+    my $self = shift;
+    $self->set_authorized_by(Jifty->web->current_user->id);
+}
+
+=head2 can_trade_for_access_token
+
+This neatly encapsulates the "is this request token perfect?" check.
+
+This will return a (boolean, message) pair, with boolean indicating success
+(true means the token is good) and message indicating error (or another
+affirmation of success).
+
+=cut
+
+sub can_trade_for_access_token {
+    my $self = shift;
+
+    return (0, "Request token is not authorized")
+        if !$self->authorized;
+
+    return (0, "Request token does not have an authorizing user")
+        if !$self->authorized_by;
+
+    return (0, "Request token already used")
+        if $self->used;
+
+    return (0, "Request token expired")
+        if $self->valid_until < DateTime->now;
+
+    return (1, "Request token valid");
+}
+
+1;
+

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Token.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/Token.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,65 @@
+#!/usr/bin/env perl
+package Jifty::Plugin::OAuth::Token;
+use strict;
+use warnings;
+use Scalar::Util 'blessed';
+
+=head1 DESCRIPTION
+
+This just provides some helper methods for both token classes to use
+
+=cut
+
+=head2 generate_token
+
+This will create a randomly generated 20-character token for use as
+a request or access token. The string is hexadecimal.
+
+This does not check for uniqueness.
+
+=cut
+
+sub generate_token {
+    return join '', map { unpack('H2', chr(int rand 256)) } 1..10;
+}
+
+=head2 before_create
+
+This does some checks and provides some defaults.
+
+It tries a number of times to create a unique C<token> using C<generate_token>.
+If that fails, this method will DIE.
+
+It will also create a secret using C<generate_token>.
+
+Finally, it will create a default C<valid_until> of 1 hour from now.
+
+=cut
+
+sub before_create {
+    my ($self, $attr) = @_;
+
+    # attempt 20 times to create a unique token string
+    for (1..20) {
+        $attr->{token} = generate_token();
+        my $token = $self->new(current_user => Jifty::CurrentUser->superuser);
+        $token->load_by_cols(token => $attr->{token});
+        last if !$token->id;
+        delete $attr->{token};
+    }
+    if (!defined $attr->{token}) {
+        die "Failed 20 times to create a unique token. Giving up.";
+        return;
+    }
+
+    # generate a secret. need not be unique, just hard to guess
+    $attr->{secret} = generate_token();
+
+    # default the lifetime of this token to 1 hour
+    $attr->{valid_until} ||= DateTime->now->add(hours => 1);
+
+    return 1;
+}
+
+1;
+

Added: jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/View.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/OAuth/View.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,170 @@
+package Jifty::Plugin::OAuth::View;
+use strict;
+use warnings;
+
+use Jifty::View::Declare -base;
+
+=head1 NAME
+
+Jifty::Plugin::OAuth::View - Views for OAuth-ey bits
+
+=cut
+
+=head2 oauth/response
+
+Internal template. Do not use.
+
+It returns OAuth parameters to the consumer in the HTTP response body.
+
+=cut
+
+template 'oauth/response' => sub {
+    my $params = get 'oauth_response';
+    if (ref($params) eq 'HASH') {
+        outs_raw join '&',
+                 map { sprintf '%s=%s',
+                       map { Jifty->web->escape_uri($_) }
+                       $_, $params->{$_}
+                 } keys %$params;
+    }
+};
+
+=head2 oauth
+
+An OAuth description page very much geared towards Consumers, since they'll
+most likely be the only ones visiting yourapp.com/oauth
+
+=cut
+
+template 'oauth' => page {
+    p {
+        b { a { attr { href => "http://oauth.net/" } "OAuth" } };
+        outs " is an open protocol to allow secure authentication to users' private data."
+    }
+
+    p {
+        "This application supports OAuth. If you'd like to access the private resources of users of this site, you must first establish a Consumer Key, Consumer Secret, and, if applicable, RSA public key with us. You can do so by contacting " . (Jifty->config->framework('AdminEmail')||'us') . ".";
+    }
+
+    p {
+        "Once you have a Consumer Key and Consumer Secret, you may begin letting users grant you access to our site. The relevant URLs are:"
+    }
+
+    dl {
+        dt {
+            outs "Request a Request Token";
+            dd { Jifty->web->url(path => '/oauth/request_token') }
+        }
+        dt {
+            outs "Obtain user authorization for a Request Token";
+            dd { Jifty->web->url(path => '/oauth/authorize') }
+        }
+        dt {
+            outs "Exchange a Request Token for an Access Token";
+            dd { Jifty->web->url(path => '/oauth/access_token') }
+        }
+    }
+
+    p {
+        my $restful = 0;
+        for (@{ Jifty->config->framework('Plugins') }) {
+            if (defined $_->{REST}) {
+                $restful = 1;
+                last;
+            }
+        }
+
+        outs "While you have a valid access token, you may browse the site as the user normally does.";
+
+        if ($restful) {
+            outs " You may also use our REST interface. See ";
+            a {
+                attr { href => Jifty->web->url(path => '=/help') }
+                Jifty->web->url(path => '=/help')
+            }
+        }
+    }
+};
+
+=head2 oauth/authorize
+
+This is the page that Users see when authorizing a request token. It renders
+the "insert token here" textbox if the consumer didn't put the request token
+in the GET query, and (always) renders Allow/Deny buttons.
+
+=cut
+
+template 'oauth/authorize' => page { title => 'Someone wants stuff!' }
+content {
+    show '/oauth/help';
+
+    my $authorize = Jifty->web->new_action(
+        moniker => 'authorize_request_token',
+        class   => 'AuthorizeRequestToken',
+    );
+
+    Jifty->web->form->start( call => get 'next' );
+
+    # if the site put the token in the request, then use it
+    # otherwise, prompt the user for it
+    my %args;
+    my $token = get 'token';
+    if ($token) {
+        $args{token} = $token;
+    }
+    else {
+        $authorize->form_field('token')->render;
+    }
+
+    outs_raw($authorize->button(
+        label => 'Allow',
+        arguments => { %args, authorize => 'allow' },
+    ));
+
+    outs_raw($authorize->button(
+        label => 'Deny',
+        arguments => { %args, authorize => 'deny' },
+    ));
+
+    Jifty->web->form->end();
+};
+
+=head2 oauth/help
+
+This provides a very, very layman description of OAuth for users
+
+=cut
+
+private template 'oauth/help' => sub {
+    div {
+        p {
+            show '/oauth/consumer';
+            outs ' is trying to access some of your data on this site. If you trust this application, you may grant it access. Note that access is read-only and will expire in one hour.';
+        }
+        p {
+            "If you're at all uncomfortable with the idea of someone rifling through your things, click Deny."
+        }
+    }
+};
+
+=head2 oauth/consumer
+
+Renders the consumer's name, and if available, its URL as a link.
+
+=cut
+
+private template 'oauth/consumer' => sub {
+    my $consumer = (get 'consumer') || 'Some application';
+
+    span {
+        outs ref($consumer) ? $consumer->name : $consumer;
+        if (ref($consumer) && $consumer->url) {
+            outs ' <';
+            a { attr { href => $consumer->url } $consumer->url };
+            outs ' >';
+        }
+    }
+};
+
+1;
+

Modified: jifty/branches/virtual-models/lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Plugin/SkeletonApp/Dispatcher.pm	Sat Oct 20 16:01:19 2007
@@ -37,5 +37,10 @@
     return ();
 };
 
+before '**' => run {
+    if (my $lang = Jifty->web->request->arguments->{_jifty_lang}) {
+        Jifty->web->session->set(jifty_lang => $lang);
+    }
+};
 
 1;

Modified: jifty/branches/virtual-models/lib/Jifty/Schema.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Schema.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Schema.pm	Sat Oct 20 16:01:19 2007
@@ -293,21 +293,21 @@
 sub connect_to_db_for_management {
     my $handle = Jifty::Handle->new();
 
-    my $driver   = Jifty->config->framework('Database')->{'Driver'};
+    my $driver = Jifty->config->framework('Database')->{'Driver'};
 
     # Everything but the template1 database is assumed
     my %connect_args;
-    $connect_args{'database'} = 'template1' if ( $handle->isa("Jifty::DBI::Handle::Pg") );
-    $connect_args{'database'} = ''          if ( $handle->isa("Jifty::DBI::Handle::mysql") );
-       for(1..50) {
-		my $counter = $_;
-    eval { 
-    $handle->connect(%connect_args);
-    };
-    my $err = $@;
-	    last if( !$err || $err =~ /does not exist/i);
-	sleep 1;
-        }
+    $connect_args{'database'} = 'template1'
+        if ( $handle->isa("Jifty::DBI::Handle::Pg") );
+    $connect_args{'database'} = ''
+        if ( $handle->isa("Jifty::DBI::Handle::mysql") );
+    for ( 1 .. 50 ) {
+        my $counter = $_;
+        eval { $handle->connect(%connect_args); };
+        my $err = $@;
+        last if ( !$err || $err =~ /does not exist/i );
+        sleep 1;
+    }
     return $handle;
 }
 

Modified: jifty/branches/virtual-models/lib/Jifty/Script/Po.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Script/Po.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Script/Po.pm	Sat Oct 20 16:01:19 2007
@@ -5,6 +5,7 @@
 use base qw(App::CLI::Command Class::Accessor::Fast);
 
 use File::Copy ();
+use File::Path 'mkpath';
 use Jifty::Config ();
 use Jifty::YAML ();
 use Locale::Maketext::Extract ();
@@ -28,14 +29,25 @@
 
 =head2 options
 
-This script only takes one option, C<--language>, which is optional; it is
-the name of a message catalog to create.  
+This script an option, C<--language>, which is optional; it is the
+name of a message catalog to create.
+
+It also takes C<--dir> to specify additional directories to extract
+from.
+
+If C<--js> is given, other options are ignored and the script will
+generate json files for each language under
+F<share/web/static/js/dict> from the current po files.  Before doing
+so, you might want to run C<jifty po> with C<--dir share/web/static/js>
+to include messages from javascript in your po files.
 
 =cut
 
 sub options {
     (
      'l|language=s' => 'language',
+     'dir=s@'       => 'directories',
+     'js'           => 'js',
     )
 }
 
@@ -48,9 +60,46 @@
 
 
 sub run {
-        my $self = shift;
-            Jifty->new(no_handle => 1);
-        $self->update_catalogs;
+    my $self = shift;
+    Jifty->new(no_handle => 1);
+
+    return $self->_js_gen if $self->{js};
+
+    $self->update_catalogs;
+}
+
+sub _js_gen {
+    my $self = shift;
+    my $static_handler = Jifty::View::Static::Handler->new;
+    my $logger =Log::Log4perl->get_logger("main");
+    for my $file ( @{ Jifty::Web->javascript_libs } ) {
+        next if $file =~ m/^ext/;
+        next if $file =~ m/^yui/;
+        next if $file =~ m/^rico/;
+        my $path = $static_handler->file_path( File::Spec->catdir( 'js', $file ) ) or next;
+
+        $logger->info("Extracting messages from '$path'");
+
+        $LMExtract->extract_file( $path );
+    }
+
+    $LMExtract->set_compiled_entries;
+    $LMExtract->compile(USE_GETTEXT_STYLE);
+
+    Jifty::I18N->new;
+    mkpath ['share/web/static/js/dict'];
+    for my $lang (Jifty::I18N->available_languages) {
+        my $file = "share/web/static/js/dict/$lang.json";
+        $logger->info("Generating $file");
+        open my $fh, '>', $file or die "$file: $!";
+
+        no strict 'refs';
+        print $fh
+            Jifty::JSON::objToJson( { map { my $text = ${"Jifty::I18N::".$lang."::Lexicon"}{$_};
+                                            defined $text ? ( $_ => $text ) : () }
+                                      keys %{$LMExtract->lexicon} },
+                                    { singlequote => 1 } );
+    }
 }
 
 =head2 _check_mime_type FILENAME
@@ -124,7 +173,7 @@
 sub extract_messages {
     my $self = shift;
     # find all the .pm files in @INC
-    my @files = File::Find::Rule->file->in( Jifty->config->framework('Web')->{'TemplateRoot'}, 'lib', 'bin' );
+    my @files = File::Find::Rule->file->in( Jifty->config->framework('Web')->{'TemplateRoot'}, 'lib', 'bin', @{ $self->{directories} || [] } );
 
     my $logger =Log::Log4perl->get_logger("main");
     foreach my $file (@files) {

Modified: jifty/branches/virtual-models/lib/Jifty/Server/Prefork.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Server/Prefork.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Server/Prefork.pm	Sat Oct 20 16:01:19 2007
@@ -18,7 +18,7 @@
 
 =head2 net_server
 
-This module depends on the L<Net::Server::Prefork> module, which is part of
+This module depends on the L<Net::Server::PreFork> module, which is part of
 the L<Net::Server> CPAN distribution.
 
 =cut

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 Oct 20 16:01:19 2007
@@ -12,6 +12,8 @@
 use File::Path;
 use File::Spec;
 use File::Temp;
+use Hash::Merge;
+use Cwd 'abs_path';
 
 =head1 NAME
 
@@ -209,9 +211,7 @@
 	my $booted;
 	if (Jifty->handle && !$@) {
 	    my $baseclass = Jifty->app_class;
-	    my $schema = Jifty::Script::Schema->new;
-	    $schema->prepare_model_classes;
-	    for my $model_class ( grep {/^\Q$baseclass\E::Model::/} $schema->models ) {
+	    for my $model_class ( grep {/^\Q$baseclass\E::Model::/} Jifty::Schema->new->models ) {
 		# We don't want to get the Collections, for example.
 		next unless $model_class->isa('Jifty::DBI::Record');
 		Jifty->handle->simple_query('TRUNCATE '.$model_class->table );
@@ -238,8 +238,7 @@
 
     Jifty->new( no_handle => 1, pre_init => 1 );
 
-    my $schema = Jifty::Script::Schema->new;
-    $schema->{drop_database}     = 1;
+    my $schema = Jifty::Script::Schema->new; $schema->{drop_database}     = 1;
     $schema->{create_database}   = 1;
     $schema->{create_all_tables} = 1;
     $schema->run;
@@ -247,14 +246,88 @@
     Jifty->new();
 }
 
+=head2 load_test_configs FILENAME
+
+This will load all the test config files that apply to FILENAME (default:
+C<$0>, the current test script file). Say you are running the test script
+C</home/bob/MyApp/t/user/12-delete.t>. The files that will be loaded are:
+
+=over 4
+
+=item C</home/bob/MyApp/t/user/12-delete.t-config.yml>
+
+=item C</home/bob/MyApp/t/user/test_config.yml>
+
+=item C</home/bob/MyApp/t/test_config.yml>
+
+=back
+
+..followed by the usual Jifty configuration files (such as
+C<MyApp/etc/config.yml> and C<MyApp/etc/site_config.yml>). The options in a
+more specific test file override the options in a less specific test file.
+
+The options are returned in a single hashref.
+
+=cut
+
+sub load_test_configs {
+    my $class = shift;
+    my ($test_config_file) = @_;
+
+    # Jifty::SubTest uses chdir which screws up $0, so to be nice it also makes
+    # available the cwd was before it uses chdir.
+    my $cwd = $Jifty::SubTest::OrigCwd;
+
+    # get the initial test config file, which is the input . "-config.yml"
+    $test_config_file = $0 if !defined($test_config_file);
+    $test_config_file .= "-config.yml";
+    $test_config_file = File::Spec->rel2abs($test_config_file, $cwd);
+
+    my $test_options = _read_and_merge_config_file($test_config_file, {});
+
+    # get the directory of the input, so we can recurse upwards
+    my ($volume, $directories) = File::Spec->splitpath($test_config_file);
+    my $directory = File::Spec->catdir($volume, $directories);
+
+    my $depth = $ENV{JIFTY_TEST_DEPTH} || 30;
+
+    for (1 .. $depth)
+    {
+        my $file = File::Spec->catfile($directory, "test_config.yml");
+        $test_options = _read_and_merge_config_file($file, $test_options);
+
+        # are we at the app root? if so, then we can stop moving up
+        $directory = abs_path(File::Spec->catdir($directory, File::Spec->updir($directory)));
+        return $test_options
+            if Jifty::Util->is_app_root($directory);
+    }
+
+    Jifty->log->fatal("Stopping looking for test config files after recursing upwards $depth times. Either you have a nonstandard layout or an incredibly deep test hierarchy. If you really do have an incredibly deep test hierarchy, you can set the environment variable JIFTY_TEST_DEPTH to a larger value.");
+
+    return $test_options;
+}
+
+sub _read_and_merge_config_file {
+    my $file = shift;
+    my $config = shift;
+
+    my $file_options = Jifty::Config->load_file($file);
+
+    Hash::Merge::set_behavior('RIGHT_PRECEDENT');
+
+    # merge the new options into what we have so far
+    return Hash::Merge::merge($file_options, $config);
+}
+
 =head2 test_config
 
 Returns a hash which overrides parts of the application's
 configuration for testing.  By default, this changes the database name
 by appending a 'test', as well as setting the port to a random port
-between 10000 and 15000.
+between 10000 and 15000. Individual test configurations may override these
+defaults (see C<load_test_configs>).
 
-It is passed the current configuration.
+It is passed the current configuration before any test config is loaded.
 
 You can override this to provide application-specific test
 configuration, e.g:
@@ -268,13 +341,16 @@
         return $hash;
     }
 
+Note that this is deprecated in favor of having real config files in your
+test directory.
+
 =cut
 
 sub test_config {
     my $class = shift;
     my ($config) = @_;
 
-    return {
+    my $defaults = {
         framework => {
             Database => {
                 Database => $config->framework('Database')->{Database} . $class->_testfile_to_dbname(),
@@ -288,15 +364,23 @@
             LogLevel => 'WARN'
         }
     };
+
+    Hash::Merge::set_behavior('RIGHT_PRECEDENT');
+    return Hash::Merge::merge($defaults, $class->load_test_configs);
 }
 
 
 sub _testfile_to_dbname {
+    if ($ENV{JIFTY_FAST_TEST}) {
+        return 'fasttest';
+    }
+    else {
     my $dbname = lc($0);
     $dbname =~ s/\.t$//;
     $dbname =~ s/[-_\.\/\\]//g;
     $dbname = substr($dbname,-32,32);	
     return $dbname;
+    } 
 }
 
 =head2 make_server
@@ -357,7 +441,7 @@
 =cut
 
 sub mailbox {
-    return Jifty::Util->absolute_path("t/mailbox");
+    return Jifty::Util->absolute_path("t/mailbox_" . _testfile_to_dbname());
 }
 
 =head2 setup_mailbox

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 Oct 20 16:01:19 2007
@@ -174,6 +174,25 @@
     return ''; # returning undef causes tons of 'uninitialized...' warnings.
 }
 
+=head2 is_app_root PATH
+
+Returns a boolean indicating whether the path passed in is the same path as
+the app root. Useful if you're recursing up a directory tree and want to
+stop when you've hit the root. It does not attempt to handle symbolic links.
+
+=cut
+
+sub is_app_root
+{
+    my $self = shift;
+    my $path = shift;
+    my $app_root = $self->app_root;
+
+    my $rel = File::Spec->abs2rel( $path, $app_root );
+
+    return $rel eq File::Spec->curdir;
+}
+
 =head2 default_app_name
 
 Returns the default name of the application.  This is the name of the

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 Oct 20 16:01:19 2007
@@ -323,10 +323,12 @@
     div {
         { class is 'crud read item inline' };
         my @fields = $self->display_columns($update);
-        render_action( $update, \@fields, { render_mode => 'read' } );
-
+        foreach my $field (@fields) {
+            div { { class is 'view-argument-'.$field};
+            render_param( $update => $field,  render_mode => 'read'  );
+            }; 
+        }
         show ('./view_item_controls', $record, $update); 
-
         hr {};
     };
 
@@ -449,13 +451,14 @@
     my ( $page ) = get(qw(page ));
     my $item_path = get('item_path') || $self->fragment_for("view");
     my $collection =  $self->_current_collection();
+    div { {class is 'crud-'.$self->object_type}; 
 
     show('./search_region');
     show( './paging_top',    $collection, $page );
     show( './list_items',    $collection, $item_path );
     show( './paging_bottom', $collection, $page );
     show( './new_item_region');
-
+    };
 };
 
 =head2 per_page
@@ -643,7 +646,11 @@
 private template 'edit_item' => sub {
     my $self = shift;
     my $action = shift;
-    render_action($action, [$self->display_columns($action)]);
+   foreach my $field ($self->display_columns($action)) {
+            div { { class is 'update-argument-'.$field}
+    render_param($action, $field) ;
+        }
+   }
 };
 
 =head1 new_item

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 Oct 20 16:01:19 2007
@@ -1073,12 +1073,13 @@
 =cut
 
 sub include_css {
+    # XXX: move to CompressCSSandJS plugin
     my $self = shift;
     my ($ccjs) = Jifty->find_plugin('Jifty::Plugin::CompressedCSSandJS');
     if ( $ccjs && $ccjs->css_enabled ) {
         $self->generate_css;
         $self->out(
-            '<link rel="stylesheet" type="text/css" href="/__jifty/css/'
+            qq{<link rel="stylesheet" type="text/css" href="@{[ $ccjs->cdn ]}/__jifty/css/}
             . __PACKAGE__->cached_css_digest . '.css" />'
         );
     }

Modified: jifty/branches/virtual-models/lib/Jifty/Web/Form/Clickable.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Web/Form/Clickable.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Web/Form/Clickable.pm	Sat Oct 20 16:01:19 2007
@@ -3,6 +3,7 @@
 
 package Jifty::Web::Form::Clickable;
 use Class::Trigger;
+use Scalar::Util qw/blessed/;
 
 =head1 NAME
 
@@ -84,9 +85,26 @@
 
 A list of actions to run when the object is clicked.  This may be an
 array refrence or a single element; each element may either be a
-moniker or a L<Jifty::Action>.  An undefined value submits B<all>
-actions in the form, an empty list reference (the default) submits
-none.
+moniker or, a L<Jifty::Action> or a hashref with the keys 'action' and 'arguments'. 
+An undefined value submits B<all> actions in the form, an empty list 
+reference (the default) submits none.
+
+In the most complex case, you have something like this:
+
+    submit => [
+                  {   action    => $my_action,
+                      arguments => {
+                          name => 'Default McName',
+                          age  => '23'
+                      },
+                  },
+                  $my_other_action,
+                  'some-other-action-moniker'
+              ]
+
+If you specify arguments in the submit block for a button, they will override 
+any values from form fileds submitted by the user.
+
 
 =item preserve_state
 
@@ -166,11 +184,35 @@
     }
 
     if ( $self->{submit} ) {
-        $self->{submit} = [ $self->{submit} ] unless ref $self->{submit} eq "ARRAY";
-        $self->{submit}
-            = [ map { ref $_ ? $_->moniker : $_ } @{ $self->{submit} } ];
+        $self->{submit} = [ $self->{submit} ]
+            unless ref $self->{submit} eq "ARRAY";
+
+        my @submit_temp = ();
+        foreach my $submit ( @{ $self->{submit} } ) {
+
+       # If we have been handed an action moniker to submit, just submit that.
+            if ( !ref($submit) ) { push @submit_temp, $submit }
+
+            # We've been handed a Jifty::Action to submit
+            elsif ( blessed($submit) ) { push @submit_temp, $submit->moniker }
+
+          # We've been handed a hashref which contains an action and arguments
+            else {
+
+           # Add whatever additional arguments they've requested to the button
+                $args{parameters}{ $submit->{'action'}->form_field_name($_) }
+                    = $submit->{arguments}{$_}
+                    for keys %{ $submit->{arguments} };
+
+                # Add the action's moniker to the submit
+                push @submit_temp, $submit->{'action'}->moniker;
+            }
+        }
+
+        @{ $self->{submit} } = @submit_temp;
     }
 
+
     # Anything doing fragment replacement needs to preserve the
     # current state as well
     if ( grep { $self->$_ } $self->handlers or $self->preserve_state ) {

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 Oct 20 16:01:19 2007
@@ -2,6 +2,7 @@
 use strict;
 
 package Jifty::Web::Form::Element;
+use Scalar::Util qw/blessed/;
 
 =head1 NAME
 
@@ -84,8 +85,14 @@
 
 =item submit => MONIKER
 
-An action, moniker of an action, or array reference to such; these
-actions are submitted when the event is fired.
+A Jifty::Action, Jifty::Action moniker, hashref of 
+    { action => Jifty::Action::Subclass, 
+     arguments => { argument => value, argument2 => value2 }
+
+or an arrayref of them.
+
+These actions are submitted when the event is fired. Any arguments 
+specified will override arguments submitted by form field.
 
 =item disable => BOOLEAN
 
@@ -397,18 +404,34 @@
         # since Jifty::Action caches instances of Jifty::Web::Form::Clickable.
         if ( $hook->{submit} ) {
             $hook->{submit} = [ $hook->{submit} ] unless ref $hook->{submit} eq "ARRAY";
-            $hook->{submit} = [ map { ref $_ ? $_->moniker : $_ } @{ $hook->{submit} } ];
+
+            my @submit_tmp;
+            foreach my $submit ( @{$hook->{submit}}) {
+                if (!ref($submit)){ 
+                        push @submit_tmp, $submit;
+                    } 
+                elsif(blessed($submit)) {
+                        push @submit_tmp, $submit->moniker;
+
+                } else { # it's a hashref
+                        push @submit_tmp, $submit->{'action'}->moniker;
+                        $hook->{'action_arguments'}->{ $submit->{'action'}->moniker } = $submit->{'arguments'};
+                }
+
+            }
+
+            @{$hook->{submit}} =  @submit_tmp;
         }
 
+        $hook->{args} ||= $hook->{arguments}; # should be able to use 'arguments' and not lose.
+
         if ( $hook->{args} ) {
             # We're going to pass complex query mapping structures
             # as-is to the server, but we need to make sure we're not
             # trying to pass around Actions, merely their monikers.
             for my $key ( keys %{ $hook->{args} } ) {
                 next unless ref $hook->{args}{$key} eq "HASH";
-                $hook->{args}{$key}{$_} = $hook->{args}{$key}{$_}->moniker
-                  for grep { ref $hook->{args}{$key}{$_} }
-                  keys %{ $hook->{args}{$key} };
+                $hook->{args}{$key}{$_} = $hook->{args}{$key}{$_}->moniker for grep { ref $hook->{args}{$key}{$_} } keys %{ $hook->{args}{$key} };
             }
         } else {
             $hook->{args} = {};
@@ -442,7 +465,7 @@
         my $actions = {};    # Maps actions => disable?
         my $confirm;
         my $beforeclick;
-
+        my $action_arguments = {};
         for my $hook (grep {ref $_ eq "HASH"} (@{$value})) {
 
             if (!($self->handler_allowed($trigger))) {
@@ -462,7 +485,11 @@
                 my $disable_form_on_click = exists $hook->{disable} ? $hook->{disable} : 1;
                 # Normalize to 1/0 to pass to JS
                 $disable_form_on_click = $disable_form_on_click ? 1 : 0;
-                $actions->{$_} = $disable_form_on_click for (@{ $hook->{submit} || [] }); 
+                for (@{ $hook->{submit} || [] }) {
+                    $actions->{$_} = $disable_form_on_click;
+                    $action_arguments->{$_} = $hook->{'action_arguments'}->{$_};
+                }
+
             }
 
             $hook->{region} ||= Jifty->web->qualified_region;
@@ -530,11 +557,25 @@
         }
 
         my $string = join ";", (grep {not ref $_} (ref $value eq "ARRAY" ? @{$value} : ($value)));
-        if (@fragments or (!$actions || %$actions)) {
+        if ( @fragments or ( !$actions || %$actions ) ) {
 
-            my $update = Jifty->web->escape("update( ". Jifty::JSON::objToJson( {actions => $actions, fragments => \@fragments, continuation => $self->continuation }, {singlequote => 1}) .", this );");
-            $string .= 'if(event.ctrlKey||event.metaKey||event.altKey||event.shiftKey) return true; ' if ($trigger eq 'onclick');
-            $string .= $self->javascript_preempt ? "return $update" : "$update; return true;";
+            my $update = Jifty->web->escape(
+                "update( "
+                    . Jifty::JSON::objToJson(
+                    {   actions      => $actions,
+                        action_arguments => $action_arguments,
+                        fragments    => \@fragments,
+                        continuation => $self->continuation
+                    },
+                    { singlequote => 1 }
+                    ) . ", this );"
+            );
+            $string
+                .= 'if(event.ctrlKey||event.metaKey||event.altKey||event.shiftKey) return true; '
+                if ( $trigger eq 'onclick' );
+            $string .= $self->javascript_preempt
+                ? "return $update"
+                : "$update; return true;";
         }
         if ($confirm) {
             $string = Jifty->web->escape("if(!confirm(" . Jifty::JSON::objToJson($confirm, {singlequote => 1}) . ")) { Event.stop(event); return false }") . $string;

Modified: jifty/branches/virtual-models/lib/Jifty/Web/Menu.pm
==============================================================================
--- jifty/branches/virtual-models/lib/Jifty/Web/Menu.pm	(original)
+++ jifty/branches/virtual-models/lib/Jifty/Web/Menu.pm	Sat Oct 20 16:01:19 2007
@@ -233,7 +233,7 @@
     my @kids = $self->children;
     my $id   = Jifty->web->serial;
     Jifty->web->out( qq{<li class="toplevel }
-            . ( $self->active ? 'active' : 'closed' ) . qq{">}
+            . ( $self->active ? 'active' : 'closed' ) .' '.$self->class.' '. qq{">}
             . qq{<span class="title">} );
     Jifty->web->out( $self->as_link );
     Jifty->web->out(qq{</span>});
@@ -246,7 +246,7 @@
                 . $id
                 . qq{">} );
         for (@kids) {
-            Jifty->web->out(qq{<li class="submenu }.($_->active ? 'active' : '' ).qq{">});
+            Jifty->web->out(qq{<li class="submenu }.($_->active ? 'active' : '' ).' '. $_->class.qq{">});
 
             # We should be able to get this as a string.
             # Either stringify the link object or output the label

Modified: jifty/branches/virtual-models/share/plugins/Jifty/Plugin/AutoReference/web/static/js/autoreference.js
==============================================================================
--- jifty/branches/virtual-models/share/plugins/Jifty/Plugin/AutoReference/web/static/js/autoreference.js	(original)
+++ jifty/branches/virtual-models/share/plugins/Jifty/Plugin/AutoReference/web/static/js/autoreference.js	Sat Oct 20 16:01:19 2007
@@ -34,5 +34,32 @@
     afterUpdate: function(field, selection) {
         
         Form.Element.validate(this.hiddenField);
+    },
+
+    getUpdatedChoices: function() {
+        var request = { path: this.url, actions: {} };
+
+        var a = $H();
+        a['moniker'] = 'autocomplete';
+        a['class']   = 'Jifty::Action::Autocomplete';
+        a['fields']  = $H();
+        a['fields']['moniker']  = this.action.moniker;
+        a['fields']['argument'] = Form.Element.getField(this.field);
+        request['actions']['autocomplete'] = a;
+        request['actions'][this.action.moniker] = this.action.data_structure();
+        request['actions'][this.action.moniker]['active']  = 0;
+
+        // Fix up the field to use the real field instead of the hidden one
+        request['actions'][this.action.moniker]['fields'][a['fields']['argument']]['value'] = this.field.value;
+
+        var options = { postBody: JSON.stringify(request),
+            onComplete: this.onComplete.bind(this),
+            requestHeaders: ['Content-Type', 'text/x-json']
+        };
+
+        new Ajax.Request(this.url,
+                options
+                );
     }
+
 });

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 Oct 20 16:01:19 2007
@@ -17,7 +17,7 @@
 function _get_named_args(args) {
     var result = {};
     for (var i = 0; i < args.length; i+=2) {
-	result[args[i]] = args[i+1];
+        result[args[i]] = args[i+1];
     }
     return result;
 
@@ -26,11 +26,11 @@
 function _get_onclick(action_hash, name, args, path) {
     var onclick = 'if(event.ctrlKey||event.metaKey||event.altKey||event.shiftKey) return true; return update('
     + JSON.stringify({'continuation': {},
-		      'actions': action_hash,
-		      'fragments': [{'mode': 'Replace', 'args': args, 'region': name, 'path': path}]})
+                      'actions': action_hash,
+                      'fragments': [{'mode': 'Replace', 'args': args, 'region': name, 'path': path}]})
     +', this)';
     onclick = onclick.replace(/"/g, "'"); //"' )# grr emacs!
-	return onclick;
+        return onclick;
 }
 // XXX
 var hyperlink  = function() {
@@ -38,9 +38,9 @@
     var current_region = Jifty.Web.current_region;
     var onclick = _get_onclick({}, current_region.name, current_region.args, args.onclick[0].replace_with);
     outs( a(function() { attr(function()
-			      {return ['onclick', onclick, 'href', '#']});
-	    return args.label
-		}));
+                              {return ['onclick', onclick, 'href', '#']});
+            return args.label
+                }));
 }
 
 var render_param = function(a, field) { outs(a.render_param(field)) };
@@ -57,27 +57,27 @@
     var current_region = Jifty.Web.current_region;
     var onclick = _get_onclick(action_hash, current_region.name, current_region.args, current_region.path);
     outs(
-	 div(function() {
-		 attr(function() { return ['class', 'submit_button'] });
-		 return input(function() { attr(function()
-						{return ['type', 'submit',
-							 'onclick', onclick,
-							 'class', 'widget button',
-							 'id', 'S' + (++SERIAL + SERIAL_postfix),
-							 'value', args.label,
-							 'name', 'J:V-region-__page-signup_widget=_signup|J:ACTIONS=signupnow'] })});
-		     }));
+         div(function() {
+                 attr(function() { return ['class', 'submit_button'] });
+                 return input(function() { attr(function()
+                                                {return ['type', 'submit',
+                                                         'onclick', onclick,
+                                                         'class', 'widget button',
+                                                         'id', 'S' + (++SERIAL + SERIAL_postfix),
+                                                         'value', args.label,
+                                                         'name', 'J:V-region-__page-signup_widget=_signup|J:ACTIONS=signupnow'] })});
+                     }));
 
 };
 
 function register_action(a) {
     outs(div(function() {
-		attr(function() { return ['class', 'hidden'] });
-		return input(function() { attr(function() {
-				return ['type', 'hidden',
-					'name', a.register_name(),
-					'id', a.register_name(),
-					'value', a.actionClass] }) } ) } ));
+                attr(function() { return ['class', 'hidden'] });
+                return input(function() { attr(function() {
+                                return ['type', 'hidden',
+                                        'name', a.register_name(),
+                                        'id', a.register_name(),
+                                        'value', a.actionClass] }) } ) } ));
     /* XXX: fallback values */
 }
 
@@ -335,23 +335,23 @@
 
     /* client side logic extracted from Jifty::Action */
     _action_spec: function() {
-	if (!this.s_a) {
-	    /* XXX: make REST client accessible */
-	    var Todo = new AsynapseRecord('todo');
-	    this.s_a = $H(Todo.eval_ajax_get('/=/action/'+this.actionClass+'.js'));
-	}
-	
-	return this.s_a
+        if (!this.s_a) {
+            /* XXX: make REST client accessible */
+            var Todo = new AsynapseRecord('todo');
+            this.s_a = $H(Todo.eval_ajax_get('/=/action/'+this.actionClass+'.js'));
+        }
+        
+        return this.s_a
     },
     argument_names: function() {
-	return this._action_spec().keys();
+        return this._action_spec().keys();
     },
 
     render_param: function(field) {
-	var a_s = this._action_spec();
-	var type = 'text';
-	var f = new ActionField(field, a_s[field], this);
-	return f.render();
+        var a_s = this._action_spec();
+        var type = 'text';
+        var f = new ActionField(field, a_s[field], this);
+        return f.render();
     },
     register_name: function() { return this.register.id }
 
@@ -362,110 +362,110 @@
 ActionField = Class.create();
 ActionField.prototype = {
  initialize: function(name, args, action) {
-	this.name = name;
-	this.label = args.label;
-	this.hints = args.hints;
-	this.mandatory = args.mandatory;
-	this.ajax_validates = args.ajax_validates;
-	this.current_value = action.data_structure().fields[name].value;
+        this.name = name;
+        this.label = args.label;
+        this.hints = args.hints;
+        this.mandatory = args.mandatory;
+        this.ajax_validates = args.ajax_validates;
+        this.current_value = action.data_structure().fields[name].value;
         this.error = action.result.field_error[name];
-	this.action = action;
-	if (!this.render_mode) this.render_mode = 'update';
-	this.type = 'text';
+        this.action = action;
+        if (!this.render_mode) this.render_mode = 'update';
+        this.type = 'text';
     },
 
  render: function() {
-	if (this.render_mode == 'read')
-	    return this.render_wrapper
-		(this.render_preamble,
-		 this.render_label,
-		 this.render_value);
-	else
-	    return this.render_wrapper
-	    (this.render_preamble,
-	     this.render_label,
-	     this.render_widget,
-	     this.render_autocomplete_div,
-	     this.render_inline_javascript,
-	     this.render_hints,
-	     this.render_errors,
-	     this.render_warnings,
-	     this.render_canonicalization_notes);
+        if (this.render_mode == 'read')
+            return this.render_wrapper
+                (this.render_preamble,
+                 this.render_label,
+                 this.render_value);
+        else
+            return this.render_wrapper
+            (this.render_preamble,
+             this.render_label,
+             this.render_widget,
+             this.render_autocomplete_div,
+             this.render_inline_javascript,
+             this.render_hints,
+             this.render_errors,
+             this.render_warnings,
+             this.render_canonicalization_notes);
     },
  render_wrapper: function () {
-	var classes = ['form_field'];
-	if (this.mandatory) classes.push('mandatory');
-	if (this.name) classes.push('argument-'+this.name);
-	var args = arguments;
-	var tthis = this;
-	return div(function() {
-		attr(function(){return ['class', classes.join(' ')]});
-		var buf = new Array;
-		for (var i = 0; i < args.length; ++i) {
-		    buf.push(typeof(args[i]) == 'function' ? args[i].apply(tthis) : args[i]);
-		}
-		return buf.join('');
-	    });
+        var classes = ['form_field'];
+        if (this.mandatory) classes.push('mandatory');
+        if (this.name) classes.push('argument-'+this.name);
+        var args = arguments;
+        var tthis = this;
+        return div(function() {
+                attr(function(){return ['class', classes.join(' ')]});
+                var buf = new Array;
+                for (var i = 0; i < args.length; ++i) {
+                    buf.push(typeof(args[i]) == 'function' ? args[i].apply(tthis) : args[i]);
+                }
+                return buf.join('');
+            });
     },
     render_preamble: function() {
-	var tthis = this;
-	return span(function(){attr(function(){return ['class', "preamble"]});
-		return tthis.preamble });
+        var tthis = this;
+        return span(function(){attr(function(){return ['class', "preamble"]});
+                return tthis.preamble });
     },
 
     render_label: function() {
-	var tthis = this;
-	if(this.render_mode == 'update')
-	    return label(function(){attr(function(){return['class', "label", 'for', tthis.element_id()]});
-		    return tthis.label });
-	else
-	    return span(function(){attr(function(){return['class', "label" ]});
-		    return tthis.label });
+        var tthis = this;
+        if(this.render_mode == 'update')
+            return label(function(){attr(function(){return['class', "label", 'for', tthis.element_id()]});
+                    return tthis.label });
+        else
+            return span(function(){attr(function(){return['class', "label" ]});
+                    return tthis.label });
     },
  input_name: function() {
-	return ['J:A:F', this.name, this.action.moniker].join('-');
+        return ['J:A:F', this.name, this.action.moniker].join('-');
     },
  render_hints: function() {
-	var tthis = this;
-	return span(function(){attr(function(){return ['class', "hints"]});
-		return tthis.hints });
+        var tthis = this;
+        return span(function(){attr(function(){return ['class', "hints"]});
+                return tthis.hints });
     },
 
  render_errors: function() {
-	if (!this.action) return '';
-	var tthis = this;
-	// XXX: post-request handler needs to extract field error messages
-	return span(function(){attr(function(){return ['class', "error", 'id', 'errors-'+tthis.input_name()]});
-		return tthis.error });
+        if (!this.action) return '';
+        var tthis = this;
+        // XXX: post-request handler needs to extract field error messages
+        return span(function(){attr(function(){return ['class', "error", 'id', 'errors-'+tthis.input_name()]});
+                return tthis.error });
     },
 
  render_widget: function () {
-	var tthis = this;
-	return input(function(){
-		    attr(function(){
-			    var fields = ['type', tthis.type];
-			    if (tthis.input_name) fields.push('name', tthis.input_name());
-			    fields.push('id', tthis.element_id());
-			    if (tthis.current_value) fields.push('value', tthis.current_value);
-			    fields.push('class', tthis._widget_class().join(' '));
-			    if (tthis.max_length) fields.push('size', tthis.max_length, 'maxlength', tthis.max_length);
-			    if (tthis.disable_autocomplete) fields.push('autocomplete', "off");
-			    //" " .$self->other_widget_properties;
-			    return fields;
-			})});
+        var tthis = this;
+        return input(function(){
+                    attr(function(){
+                            var fields = ['type', tthis.type];
+                            if (tthis.input_name) fields.push('name', tthis.input_name());
+                            fields.push('id', tthis.element_id());
+                            if (tthis.current_value) fields.push('value', tthis.current_value);
+                            fields.push('class', tthis._widget_class().join(' '));
+                            if (tthis.max_length) fields.push('size', tthis.max_length, 'maxlength', tthis.max_length);
+                            if (tthis.disable_autocomplete) fields.push('autocomplete', "off");
+                            //" " .$self->other_widget_properties;
+                            return fields;
+                        })});
     },
  _widget_class: function() {
-	var classes = ['form_field'];
-	if (this.mandatory)      classes.push('mandatory');
-	if (this.name)           classes.push('argument-'+this.name);
-	if (this.ajax_validates) classes.push('ajaxvalidation');
-	return classes;
+        var classes = ['form_field'];
+        if (this.mandatory)      classes.push('mandatory');
+        if (this.name)           classes.push('argument-'+this.name);
+        if (this.ajax_validates) classes.push('ajaxvalidation');
+        return classes;
     },
 
  element_id: function() { if(!this._element_id) this._element_id = this.input_name() + '-S' + (++SERIAL + SERIAL_postfix);
-			  return this._element_id; },
+                          return this._element_id; },
  __noSuchMethod__: function(name) {
-	return '<!-- '+name+' not implemented yet -->';
+        return '<!-- '+name+' not implemented yet -->';
     }
 
 };
@@ -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[moniker])
+            current_actions[moniker] = new Action(moniker);
+        return current_actions[moniker];
     },
 
     // Returns the name of the field
@@ -900,7 +900,7 @@
          child = child.nextSibling) {
         var name = child.nodeName.toLowerCase();
         if (table[name])
-	    table[name](child);
+            table[name](child);
     }
 }
 
@@ -914,14 +914,14 @@
             } else if (fragment_bit.firstChild) {
                 textContent = fragment_bit.firstChild.nodeValue;
             } 
-	    try {
-		var cache_func = eval(textContent);
-		CACHE[f['path']] = { 'type': c_type, 'content': cache_func };
-	    }
-	    catch(e) { 
-		alert(e);
-		alert(textContent);
-	    }
+            try {
+                var cache_func = eval(textContent);
+                CACHE[f['path']] = { 'type': c_type, 'content': cache_func };
+            }
+            catch(e) { 
+                alert(e);
+                alert(textContent);
+            }
         }
     });
 };
@@ -937,61 +937,63 @@
     var element = f['element'];
     walk_node(fragment,
     { argument: function(fragment_bit) {
-	    // First, update the fragment's arguments
-	    // with what the server actually used --
-	    // this is needed in case there was
-	    // argument mapping going on
-	    var textContent = '';
-	    if (fragment_bit.textContent) {
-		textContent = fragment_bit.textContent;
-	    } else if (fragment_bit.firstChild) {
-		textContent = fragment_bit.firstChild.nodeValue;
-	    }
-	    new_dom_args[fragment_bit.getAttribute("name")] = textContent;
-	},
+            // First, update the fragment's arguments
+            // with what the server actually used --
+            // this is needed in case there was
+            // argument mapping going on
+            var textContent = '';
+            if (fragment_bit.textContent) {
+                textContent = fragment_bit.textContent;
+            } else if (fragment_bit.firstChild) {
+                textContent = fragment_bit.firstChild.nodeValue;
+            }
+            new_dom_args[fragment_bit.getAttribute("name")] = textContent;
+        },
       content: function(fragment_bit) {
-	    var textContent = '';
-	    if (fragment_bit.textContent) {
-		textContent = fragment_bit.textContent;
-	    } else if (fragment_bit.firstChild) {
-		textContent = fragment_bit.firstChild.nodeValue;
-	    }
+            var textContent = '';
+            if (fragment_bit.textContent) {
+                textContent = fragment_bit.textContent;
+            } else if (fragment_bit.firstChild) {
+                textContent = fragment_bit.firstChild.nodeValue;
+            }
                     
-	    // Once we find it, do the insertion
-	    if (f['mode'] && (f['mode'] != 'Replace')) {
-		var insertion = eval('Insertion.'+f['mode']);
-		new insertion(element, textContent.stripScripts());
-	    } else {
-		Element.update(element, textContent.stripScripts());
-	    }
-	    // We need to give the browser some "settle" time before
-	    // we eval scripts in the body
+            // Once we find it, do the insertion
+            if (f['mode'] && (f['mode'] != 'Replace')) {
+                var insertion = eval('Insertion.'+f['mode']);
+                new insertion(element, textContent.stripScripts());
+            } else {
+                Element.update(element, textContent.stripScripts());
+            }
+            // We need to give the browser some "settle" time before
+            // we eval scripts in the body
         YAHOO.util.Event.onAvailable(element.id, function() {
             (function() { this.evalScripts() }).bind(textContent)();
         });
         Behaviour.apply(element);
-	}
+        }
     });
     dom_fragment.setArgs(new_dom_args);
 
     // Also, set us up the effect
     if (f['effect']) {
-	try {
-	    var effect = eval('Effect.'+f['effect']);
-	    var effect_args  = f['effect_args'] || {};
-	    if (effect) {
-		if (f['is_new'])
-		    Element.hide($('region-'+f['region']));
-		(effect)($('region-'+f['region']), effect_args);
-	    }
-	} catch ( e ) {
-	    // Don't be sad if the effect doesn't exist
-	}
+        try {
+            var effect = eval('Effect.'+f['effect']);
+            var effect_args  = f['effect_args'] || {};
+            if (effect) {
+                if (f['is_new'])
+                    Element.hide($('region-'+f['region']));
+                (effect)($('region-'+f['region']), effect_args);
+            }
+        } catch ( e ) {
+            // Don't be sad if the effect doesn't exist
+        }
     }
 }
 
 // Update a region.  Takes a hash of named parameters, including:
 //  - 'actions' is an array of monikers to submit
+//  - 'action_arguments' is a hash of action monikers to hashes of arguments which should override any arguments coming from form fields
+//        the hash keys for 'action_arguments' are the values of the 'actions' array
 //  - 'fragments' is an array of hashes, which may have:
 //     - 'region' is the name of the region to update
 //     - 'args' is a hash of arguments to override
@@ -1034,14 +1036,14 @@
     }
     var optional_fragments;
     if (form && form['J:CALL']) 
-	optional_fragments = [ prepare_element_for_update({'mode':'Replace','args':{},'region':'__page','path': null}) ];
+        optional_fragments = [ prepare_element_for_update({'mode':'Replace','args':{},'region':'__page','path': null}) ];
     // Build actions structure
     var has_request = 0;
     request['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[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')
@@ -1053,7 +1055,18 @@
             if(disable) {
                 a.disable_input_fields(disabled_elements);
             }
-            request['actions'][moniker] = a.data_structure();
+            var param = a.data_structure();
+            var fields = param.fields;
+            var override = named_args['action_arguments'][param.moniker] || {};
+            for (var argname in override) {
+                if (fields[argname]) {
+                    fields[argname].value = override[argname];
+                }
+                else {
+                    fields[argname] = { value: override[argname] };
+                }
+            }
+            request['actions'][moniker] = param;
             ++has_request;
         }
 
@@ -1072,11 +1085,11 @@
         if (cached && cached['type'] == 'static') {
             var my_fragment = document.createElement('fragment');
             var content_node = document.createElement('content');
-	    var cached_result;
+            var cached_result;
 
-	    Jifty.Web.current_region = fragments[f['region']];
-	    try { cached_result = apply_cached_for_action(cached['content'], []) }
-	    catch (e) { alert(e) }
+            Jifty.Web.current_region = fragments[f['region']];
+            try { cached_result = apply_cached_for_action(cached['content'], []) }
+            catch (e) { alert(e) }
 
             content_node.textContent = cached_result;
             my_fragment.appendChild(content_node);
@@ -1086,38 +1099,38 @@
  } );
             continue;
         }
-	else if (cached && cached['type'] == 'action') {
+        else if (cached && cached['type'] == 'action') {
             var my_fragment = document.createElement('fragment');
             var content_node = document.createElement('content');
 
             my_fragment.appendChild(content_node);
             my_fragment.setAttribute('id', f['region']);
             update_from_cache.push(function(){
-		    var cached_result;
-		    Jifty.Web.current_region = fragments[f['region']];
-		    try {
-			cached_result = apply_cached_for_action(cached['content'], Form.getActions(form));
-		    }
-		    catch (e) { alert(e); throw e }
-		    content_node.textContent = cached_result;
-		    apply_fragment_updates(my_fragment, f);
+                    var cached_result;
+                    Jifty.Web.current_region = fragments[f['region']];
+                    try {
+                        cached_result = apply_cached_for_action(cached['content'], Form.getActions(form));
+                    }
+                    catch (e) { alert(e); throw e }
+                    content_node.textContent = cached_result;
+                    apply_fragment_updates(my_fragment, f);
  } );
             continue;
-	}
+        }
         else if (cached && cached['type'] == 'crudview') {
-	    try { // XXX: get model class etc as metadata in cache 
-		// XXX: kill dup code
-	    var Todo = new AsynapseRecord('todo');
-	    var record = Todo.find(f['args']['id']);
+            try { // XXX: get model class etc as metadata in cache 
+                // XXX: kill dup code
+            var Todo = new AsynapseRecord('todo');
+            var record = Todo.find(f['args']['id']);
             var my_fragment = document.createElement('fragment');
             var content_node = document.createElement('content');
             content_node.textContent = cached['content'](record);
             my_fragment.appendChild(content_node);
             my_fragment.setAttribute('id', f['region']);
             update_from_cache.push(function(){ apply_fragment_updates(my_fragment, f); } );
-	    } catch (e) { alert(e) };
-	    continue;
-	}
+            } catch (e) { alert(e) };
+            continue;
+        }
 
         // Update with all new values
         var name = f['region'];
@@ -1145,28 +1158,28 @@
         // Grab the XML response
         var response = transport.responseXML.documentElement;
 
-	// Get action results
+        // Get action results
         walk_node(response,
-	{ result: function(result) {
-		var moniker = result.getAttribute("moniker");
-		walk_node(result,
-			  { field: function(field) {
-				  var error = field.getElementsByTagName('error')[0];
-				  if (error) {
-				      var text = error.textContent
-					  ? error.textContent
-					  : (error.firstChild ? error.firstChild.nodeValue : '');
-				      var action = current_actions[moniker];
-				      action.result.field_error[field.getAttribute("name")] = text;
-				      }
-			      }});
-	    }});
+        { result: function(result) {
+                var moniker = result.getAttribute("moniker");
+                walk_node(result,
+                          { field: function(field) {
+                                  var error = field.getElementsByTagName('error')[0];
+                                  if (error) {
+                                      var text = error.textContent
+                                          ? error.textContent
+                                          : (error.firstChild ? error.firstChild.nodeValue : '');
+                                      var action = current_actions[moniker];
+                                      action.result.field_error[field.getAttribute("name")] = text;
+                                      }
+                              }});
+            }});
 
         for ( var i = 0; i < disabled_elements.length; i++ ) {
             disabled_elements[i].disabled = false;
         }
 
-	// empty known action. XXX: we should only need to discard actions being submitted
+        // empty known action. XXX: we should only need to discard actions being submitted
 
         // Loop through the result looking for it
         var expected_fragments = optional_fragments ? optional_fragments : named_args['fragments'];
@@ -1185,20 +1198,20 @@
             extract_cacheable(response_fragment, f);
         }
 
-	update_from_cache.each(function(x) { x() });
+        update_from_cache.each(function(x) { x() });
 
         walk_node(response,
-	{ result: function(result) {
+        { result: function(result) {
                 for (var key = result.firstChild;
                      key != null;
                      key = key.nextSibling) {
                     show_action_result(result.getAttribute("moniker"),key);
                 }
             },
-	  redirect: function(redirect) {
+          redirect: function(redirect) {
                 document.location =  redirect.firstChild.firstChild.nodeValue;
-	}});
-	current_actions = $H();
+        }});
+        current_actions = $H();
     };
     var onFailure = function(transport, object) {
         hide_wait_message_now();

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/Makefile.PL
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/Makefile.PL	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,7 @@
+use inc::Module::Install;
+
+name        'TestApp::Plugin::OAuth';
+version     '0.01';
+requires    'Jifty' => '0.70824';
+
+WriteAll;

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/bin/jifty
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/bin/jifty	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+use File::Basename qw(dirname); 
+use UNIVERSAL::require;
+
+use Jifty;
+use Jifty::Script;
+
+local $SIG{INT} = sub { warn "Stopped\n"; exit; };
+Jifty::Script->dispatch();

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/etc/config.yml
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/etc/config.yml	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,51 @@
+--- 
+framework: 
+  AdminMode: 1
+  ApplicationClass: TestApp::Plugin::OAuth
+  ApplicationName: TestApp-Plugin-OAuth
+  ApplicationUUID: B5461398-7DC0-11DC-83A6-036B06D64C5E
+  ConfigFileVersion: 2
+  Database: 
+    CheckSchema: 1
+    Database: testapp_plugin_oauth
+    Driver: SQLite
+    Host: localhost
+    Password: ''
+    RecordBaseClass: Jifty::DBI::Record::Cachable
+    User: ''
+    Version: 0.0.1
+  DevelMode: 1
+  L10N: 
+    PoDir: share/po
+  LogLevel: INFO
+  Mailer: Sendmail
+  MailerArgs: []
+
+  Plugins: 
+    - REST: {}
+    - Halo: {}
+    - CompressedCSSandJS: {}
+    - AdminUI: {}
+    - OAuth: {}
+    - Authentication::Password: {}
+    - SkeletonApp: {}
+
+  PubSub: 
+    Backend: Memcached
+    Enable: ~
+  SkipAccessControl: 0
+  TemplateClass: TestApp::Plugin::OAuth::View
+  Web: 
+    BaseURL: http://localhost
+    DataDir: var/mason
+    Globals: []
+
+    MasonConfig: 
+      autoflush: 0
+      default_escape_flags: h
+      error_format: text
+      error_mode: fatal
+    Port: 8888
+    ServeStaticFiles: 1
+    StaticRoot: share/web/static
+    TemplateRoot: share/web/templates

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Dispatcher.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Dispatcher.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,25 @@
+package TestApp::Plugin::OAuth::Dispatcher;
+use strict;
+use warnings;
+use Jifty::Dispatcher -base;
+
+my @login_required = qw{
+    oauth/authorize
+};
+
+my $login_required = join '|', map {"^$_"} @login_required;
+$login_required = qr/$login_required/;
+
+before '*' => run {
+    if (Jifty->web->current_user->id) {
+        my $top = Jifty->web->navigation;
+        $top->child( _('Pick!')    => url => '/pick' );
+        $top->child( _('Choices')  => url => '/choices' );
+    }
+    elsif ($1 =~ $login_required) {
+        tangent 'login';
+    }
+};
+
+1;
+

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Model/User.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Model/User.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,11 @@
+package TestApp::Plugin::OAuth::Model::User;
+use base qw/Jifty::Record/;
+
+use Jifty::DBI::Schema;
+use Jifty::Record schema {
+    column 'tasty' =>
+        type is 'boolean';
+};
+
+1;
+

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Test.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/lib/TestApp/Plugin/OAuth/Test.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,181 @@
+#!/usr/bin/env perl
+package TestApp::Plugin::OAuth::Test;
+use strict;
+use warnings;
+use base qw/Jifty::Test/;
+
+use MIME::Base64;
+use Crypt::OpenSSL::RSA;
+use Digest::HMAC_SHA1 'hmac_sha1';
+
+our @EXPORT = qw($timestamp $url $mech $pubkey $seckey response_is sign get_latest_token);
+
+sub setup {
+    my $class = shift;
+    $class->SUPER::setup;
+    $class->export_to_level(1);
+}
+
+our $timestamp = 0;
+our $url;
+our $mech;
+our $pubkey = slurp('t/id_rsa.pub');
+our $seckey = slurp('t/id_rsa');
+
+sub response_is {
+    ++$timestamp;
+
+    my %params = (
+        oauth_timestamp        => $timestamp,
+        oauth_nonce            => scalar(reverse $timestamp),
+        oauth_signature_method => 'HMAC-SHA1',
+        oauth_version          => '1.0',
+
+        code                   => 400,
+        testname               => "",
+        method                 => 'POST',
+        token_secret           => '',
+        @_,
+    );
+
+    for (grep {!defined $params{$_}} keys %params) {
+        delete $params{$_};
+    }
+
+    my $code            = delete $params{code};
+    my $testname        = delete $params{testname} || "Response was $code";
+    my $method          = delete $params{method};
+    my $token_secret    = delete $params{token_secret};
+    my $consumer_secret = delete $params{consumer_secret}
+        or die "consumer_secret not passed to response_is!";
+
+    $params{oauth_signature} ||= sign($method, $token_secret, $consumer_secret, %params);
+
+    my $r;
+
+    if ($method eq 'POST') {
+        $r = $mech->post($url, [%params]);
+    }
+    else {
+        my $query = join '&',
+                    map { "$_=" . Jifty->web->escape_uri($params{$_}||'') }
+                    keys %params;
+        $r = $mech->get("$url?$query");
+    }
+
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+    main::is($r->code, $code, $testname);
+
+    my $token = get_latest_token();
+    if ($code == 200) {
+        main::ok($token, "Successfully loaded a token object with token ".$token->token.".");
+    }
+    else {
+        main::ok(!$token, "Did not get a token");
+    }
+}
+
+sub sign {
+    my ($method, $token_secret, $consumer_secret, %params) = @_;
+
+    local $url = delete $params{url} || $url;
+
+    my $key = delete $params{signature_key};
+    my $sig_method = $params{oauth_signature_method} || delete $params{_signature_method};
+
+    delete $params{oauth_signature};
+
+    if ($sig_method eq 'PLAINTEXT') {
+        my $signature = join '&',
+                        map { Jifty->web->escape_uri($_||'') }
+                            $consumer_secret,
+                            $token_secret;
+        return $signature;
+    }
+
+    my $normalized_request_parameters
+        = join '&',
+          map { "$_=" . Jifty->web->escape_uri($params{$_}||'') }
+          sort keys %params;
+
+    my $signature_base_string
+        = join '&',
+          map { Jifty->web->escape_uri($_||'') }
+              uc($method),
+              $url,
+              $normalized_request_parameters,
+              $consumer_secret,
+              $token_secret;
+
+    my $signature;
+
+    if ($sig_method eq 'RSA-SHA1') {
+        my $pubkey = Crypt::OpenSSL::RSA->new_private_key($key);
+        $signature = encode_base64($pubkey->sign($signature_base_string), "");
+    }
+    elsif ($sig_method eq 'HMAC-SHA1') {
+        my $key = join '&',
+          map { Jifty->web->escape_uri($_||'') }
+              $consumer_secret,
+              $token_secret;
+        my $hmac = Digest::HMAC_SHA1->new($key);
+        $hmac->add($signature_base_string);
+        $signature = $hmac->b64digest;
+    }
+
+    return ($signature, $signature_base_string, $normalized_request_parameters)
+        if wantarray;
+    return $signature;
+
+}
+
+sub slurp {
+    no warnings 'once';
+    my $file = shift;
+    local $/;
+    local @ARGV = $file;
+    my $contents = scalar <>
+        or die "Unable to slurp $file";
+    return $contents;
+}
+
+sub get_latest_token {
+    my $content = $mech->content;
+
+    $content =~ s/\boauth_token=(\w+)//
+        or return;
+    my $token = $1;
+
+    $content =~ s/\boauth_token_secret=(\w+)//
+        or return;
+    my $secret = $1;
+
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+    main::is($content, '&', "the output was exactly oauth_token=...&oauth_secret=...");
+
+    my $package = 'Jifty::Plugin::OAuth::Model::';
+
+    if ($mech->uri =~ /request_token/) {
+        $package .= 'RequestToken';
+    }
+    elsif ($mech->uri =~ /request_token/) {
+        $package .= 'AccessToken';
+    }
+    else {
+        Jifty->log->error("Called get_latest_token, but I cannot grok the URI " . $mech->uri);
+        return;
+    }
+
+    my $token_obj = $package->new(current_user => Jifty::CurrentUser->superuser);
+    $token_obj->load_by_cols(token => $token);
+
+    if (!$token_obj->id) {
+        Jifty->log->error("Could not find a $package with token $token");
+        return;
+    }
+
+    return $token_obj;
+}
+
+1;
+

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/00-test-setup.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/00-test-setup.t	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,67 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+
+use lib 't/lib';
+use Jifty::SubTest;
+
+use TestApp::Plugin::OAuth::Test;
+
+if (eval { require Net::OAuth::Request; 1 }) {
+    plan tests => 9;
+}
+else {
+    plan skip_all => "Net::OAuth isn't installed";
+}
+
+# sign PLAINTEXT {{{
+is(sign('POST', 'jjd999tj88uiths3', 'djr9rjt0jd78jf88',
+        oauth_signature_method => 'PLAINTEXT'),
+    'djr9rjt0jd78jf88&jjd999tj88uiths3', 'PLAINTEXT example 1 works');
+is(sign('POST', 'jjd99$tj88uiths3', 'djr9rjt0jd78jf88',
+        oauth_signature_method => 'PLAINTEXT'),
+    'djr9rjt0jd78jf88&jjd99%24tj88uiths3', 'PLAINTEXT example 2 works');
+is(sign('POST', undef, 'djr9rjt0jd78jf88',
+        oauth_signature_method => 'PLAINTEXT'),
+    'djr9rjt0jd78jf88&', 'PLAINTEXT example 2 works');
+# }}}
+# sign HMAC-SHA1 {{{
+my ($sig, $sbs, $nrp) = sign(
+    'GET',
+    'pfkkdhi9sl3r4s00',
+    'kd94hf93k423kf44',
+    url => 'http://photos.example.net/photos',
+    oauth_consumer_key => 'dpf43f3p2l4k3l03',
+    oauth_signature_method => 'HMAC-SHA1',
+    oauth_timestamp => '1191242096',
+    oauth_nonce => 'kllo9940pd9333jh',
+    oauth_token => 'nnch734d00sl2jdk',
+    file => 'vacation.jpg',
+    size => 'original',
+    oauth_version => '1.0');
+
+is($nrp, 'file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original', 'HMAC-SHA1 normalized request paramaters correct');
+is($sbs, 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal&kd94hf93k423kf44&pfkkdhi9sl3r4s00', 'HMAC-SHA1 signature-base-string correct');
+is($sig, 'Gcg/323lvAsQ707p+y41y14qWfY', 'HMAC-SHA1 signature correct');
+# }}}
+# sign RSA-SHA1 {{{
+($sig, $sbs, $nrp) = sign(
+    'GET',
+    'pfkkdhi9sl3r4s00',
+    'kd94hf93k423kf44',
+    url => 'http://photos.example.net/photos',
+    signature_key => $seckey,
+    oauth_consumer_key => 'dpf43f3p2l4k3l03',
+    oauth_signature_method => 'RSA-SHA1',
+    oauth_timestamp => '1191242096',
+    oauth_nonce => 'kllo9940pd9333jh',
+    oauth_token => 'nnch734d00sl2jdk',
+    file => 'vacation.jpg',
+    size => 'original',
+    oauth_version => '1.0');
+
+is($nrp, 'file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=RSA-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original', 'RSA-SHA1 normalized request paramaters correct');
+is($sbs, 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal&kd94hf93k423kf44&pfkkdhi9sl3r4s00', 'RSA-SHA1 signature-base-string correct');
+is($sig, 'oSjbUzMjD4E+LeHMaYzYx1KyULDwuR6V9oeNgTLoO9m90iJh4d01J/8SzvHKT8N0y2vs1o8s72z19Eicj6l+mEmH5Rp0cwWOE9UdvC+JdFSIA1bmlwVPCFL7jDQqRSBJsXEiT44T5j9P+Dh5Z5WUjEgCExQyNP38Z3nMnYYOCRM=', 'RSA-SHA1 signature correct');
+# }}}
+

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/01-basic.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/01-basic.t	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,33 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+
+use lib 't/lib';
+use Jifty::SubTest;
+
+use Jifty::Test;
+
+if (eval { require Net::OAuth::Request; 1 }) {
+    plan tests => 9;
+}
+else {
+    plan skip_all => "Net::OAuth isn't installed";
+}
+
+use Jifty::Test::WWW::Mechanize;
+
+my $server  = Jifty::Test->make_server;
+isa_ok($server, 'Jifty::Server');
+my $URL     = $server->started_ok;
+my $mech    = Jifty::Test::WWW::Mechanize->new();
+
+$mech->get_ok($URL . '/oauth');
+$mech->content_like(qr{/oauth/request_token}, "oauth page mentions request_token URL");
+$mech->content_like(qr{/oauth/authorize}, "oauth page mentions authorize URL");
+$mech->content_like(qr{/oauth/access_token}, "oauth page mentions access_token URL");
+
+$mech->content_like(qr{http://oauth\.net/}, "oauth page mentions OAuth homepage");
+
+$mech->get_ok($URL . '/oauth/authorize');
+$mech->content_like(qr{If you trust this application}, "oauth authorization page exists without fancy headers");
+

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/02-request-token.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/02-request-token.t	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,277 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+
+use lib 't/lib';
+use Jifty::SubTest;
+
+use TestApp::Plugin::OAuth::Test;
+
+if (eval { require Net::OAuth::Request; 1 }) {
+    plan tests => 56;
+}
+else {
+    plan skip_all => "Net::OAuth isn't installed";
+}
+
+use Jifty::Test::WWW::Mechanize;
+
+my $server  = Jifty::Test->make_server;
+isa_ok($server, 'Jifty::Server');
+my $URL     = $server->started_ok;
+$mech    = Jifty::Test::WWW::Mechanize->new();
+$url     = $URL . '/oauth/request_token';
+
+# create some consumers {{{
+my $consumer = Jifty::Plugin::OAuth::Model::Consumer->new(current_user => Jifty::CurrentUser->superuser);
+my ($ok, $msg) = $consumer->create(
+    consumer_key => 'foo',
+    secret       => 'bar',
+    name         => 'FooBar industries',
+    url          => 'http://foo.bar.example.com',
+    rsa_key      => $pubkey,
+);
+ok($ok, $msg);
+
+my $rsaless = Jifty::Plugin::OAuth::Model::Consumer->new(current_user => Jifty::CurrentUser->superuser);
+($ok, $msg) = $rsaless->create(
+    consumer_key => 'foo2',
+    secret       => 'bar2',
+    name         => 'Backwater.org',
+    url          => 'http://backwater.org',
+);
+ok($ok, $msg);
+# }}}
+
+# success modes
+
+# get a request token as a known consumer (PLAINTEXT) {{{
+response_is(
+    code                   => 200,
+    testname               => "200 - plaintext signature",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature_method => 'PLAINTEXT',
+);
+# }}}
+# get a request token as a known consumer (HMAC-SHA1) {{{
+$timestamp = 100; # set timestamp to test different consumers' timestamps
+response_is(
+    code                   => 200,
+    testname               => "200 - HMAC-SHA1 signature",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature_method => 'HMAC-SHA1',
+);
+# }}}
+# get a request token as a known consumer (RSA-SHA1) {{{
+response_is(
+    code                   => 200,
+    testname               => "200 - RSA-SHA1 signature",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    signature_key          => $seckey,
+    oauth_signature_method => 'RSA-SHA1',
+);
+# }}}
+# same timestamp, different nonce {{{
+--$timestamp;
+response_is(
+    code                   => 200,
+    testname               => "200 - RSA-SHA1 signature",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_nonce            => 'kjfh',
+    signature_key          => $seckey,
+    oauth_signature_method => 'RSA-SHA1',
+);
+# }}}
+# same nonce, different timestamp {{{
+response_is(
+    code                   => 200,
+    testname               => "200 - RSA-SHA1 signature",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_nonce            => 'kjfh',
+    signature_key          => $seckey,
+    oauth_signature_method => 'RSA-SHA1',
+);
+# }}}}
+
+# get a request token as an RSA-less consumer (PLAINTEXT) {{{
+
+# consumer 1 has a timestamp of about 101 now. if this gives a timestamp error,
+# then timestamps must be globally increasing, which is wrong. they must only
+# be increasing per consumer
+$timestamp = 50;
+
+response_is(
+    code                   => 200,
+    testname               => "200 - plaintext signature",
+    consumer_secret        => 'bar2',
+    oauth_consumer_key     => 'foo2',
+    oauth_signature_method => 'PLAINTEXT',
+);
+# }}}
+# get a request token as an RSA-less consumer (HMAC-SHA1) {{{
+response_is(
+    code                   => 200,
+    testname               => "200 - HMAC-SHA1 signature",
+    consumer_secret        => 'bar2',
+    oauth_consumer_key     => 'foo2',
+    oauth_signature_method => 'HMAC-SHA1',
+);
+# }}}
+
+# failure modes
+
+# request a request token as an RSA-less consumer (RSA-SHA1) {{{
+response_is(
+    code                   => 400,
+    testname               => "400 - RSA-SHA1 signature, without registering RSA key!",
+    consumer_secret        => 'bar2',
+    oauth_consumer_key     => 'foo2',
+    signature_key          => $seckey,
+    oauth_signature_method => 'RSA-SHA1',
+);
+# }}}
+# unknown consumer {{{
+# we're back to the first consumer, so we need a locally larger timestamp
+$timestamp = 200;
+response_is(
+    code                   => 401,
+    testname               => "401 - unknown consumer",
+    consumer_secret        => 'zzz',
+    oauth_consumer_key     => 'whoami',
+);
+# }}}
+# wrong consumer secret {{{
+response_is (
+    code                   => 401,
+    testname               => "401 - wrong consumer secret",
+    consumer_secret        => 'not bar!',
+    oauth_consumer_key     => 'foo',
+);
+# }}}
+# wrong signature {{{
+response_is(
+    code                   => 401,
+    testname               => "401 - wrong signature",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature        => 'hello ^____^',
+);
+# }}}
+# duplicate timestamp and nonce {{{
+response_is(
+    code                   => 401,
+    testname               => "401 - duplicate timestamp and nonce",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_timestamp        => 1,
+    oauth_nonce            => 1,
+    oauth_signature_method => 'PLAINTEXT',
+);
+# }}}
+# unknown signature method {{{
+response_is(
+    code                   => 400,
+    testname               => "400 - unknown signature method",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature_method => 'Peaches. Peaches FOR YOU',
+);
+# }}}
+# missing parameters {{{
+# oauth_consumer_key {{{
+response_is(
+    code                   => 400,
+    testname               => "400 - missing parameter oauth_consumer_key",
+    consumer_secret        => 'bar',
+    oauth_signature_method => 'PLAINTEXT',
+);
+# }}}
+# oauth_nonce {{{
+response_is(
+    code                   => 400,
+    testname               => "400 - missing parameter oauth_nonce",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_nonce            => undef,
+    oauth_signature_method => 'PLAINTEXT',
+);
+# }}}
+# oauth_timestamp {{{
+response_is(
+    code                   => 400,
+    testname               => "400 - missing parameter oauth_timestamp",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_timestamp        => undef,
+    oauth_signature_method => 'PLAINTEXT',
+);
+# }}}
+# oauth_signature_method {{{
+response_is(
+    code                   => 400,
+    testname               => "400 - missing parameter oauth_signature_method",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature_method => undef,
+    _signature_method       => 'PLAINTEXT', # so we get a real signature
+);
+# }}}
+# }}}
+# unsupported parameter {{{
+response_is(
+    code                   => 400,
+    testname               => "400 - unsupported parameter oauth_candy",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature_method => 'PLAINTEXT',
+    oauth_candy            => 'yummy',
+);
+# }}}
+# invalid timestamp (noninteger) {{{
+response_is(
+    code                   => 400,
+    testname               => "400 - malformed timestamp (noninteger)",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature_method => 'PLAINTEXT',
+    oauth_timestamp        => 'half past nine',
+);
+# }}}
+# invalid timestamp (smaller than previous request) {{{
+$timestamp = 1000;
+# first make a good request with a large timestamp {{{
+response_is(
+    code                   => 200,
+    testname               => "200 - setting up a future test",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+);
+# }}}
+$timestamp = 500;
+# then a new request with a smaller timestamp {{{
+response_is(
+    code                   => 401,
+    testname               => "401 - timestamp smaller than a previous timestamp",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature_method => 'PLAINTEXT',
+);
+# }}}
+$timestamp = 2000;
+# }}}
+# GET not POST {{{
+response_is(
+    code                   => 404,
+    testname               => "404 - GET not supported for request_token",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature_method => 'PLAINTEXT',
+    method                 => 'GET',
+);
+# }}}
+

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/03-authorize.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/03-authorize.t	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,54 @@
+#!/usr/bin/env perl
+use warnings;
+use strict;
+
+use lib 't/lib';
+use Jifty::SubTest;
+
+use TestApp::Plugin::OAuth::Test;
+
+if (eval { require Net::OAuth::Request; 1 }) {
+    plan tests => 7;
+}
+else {
+    plan skip_all => "Net::OAuth isn't installed";
+}
+
+use Jifty::Test::WWW::Mechanize;
+
+my $server  = Jifty::Test->make_server;
+isa_ok($server, 'Jifty::Server');
+my $URL     = $server->started_ok;
+$mech    = Jifty::Test::WWW::Mechanize->new();
+$url     = $URL . '/oauth/request_token';
+
+# create some consumers {{{
+my $consumer = Jifty::Plugin::OAuth::Model::Consumer->new(current_user => Jifty::CurrentUser->superuser);
+my ($ok, $msg) = $consumer->create(
+    consumer_key => 'foo',
+    secret       => 'bar',
+    name         => 'FooBar industries',
+    url          => 'http://foo.bar.example.com',
+    rsa_key      => $pubkey,
+);
+ok($ok, $msg);
+
+my $rsaless = Jifty::Plugin::OAuth::Model::Consumer->new(current_user => Jifty::CurrentUser->superuser);
+($ok, $msg) = $rsaless->create(
+    consumer_key => 'foo2',
+    secret       => 'bar2',
+    name         => 'Backwater.org',
+    url          => 'http://backwater.org',
+);
+ok($ok, $msg);
+# }}}
+# get a request token as a known consumer (PLAINTEXT) {{{
+response_is(
+    code                   => 200,
+    testname               => "200 - plaintext signature",
+    consumer_secret        => 'bar',
+    oauth_consumer_key     => 'foo',
+    oauth_signature_method => 'PLAINTEXT',
+);
+# }}}
+

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/id_rsa
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/id_rsa	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQC1ekM402pEiZ6MyaG0RzDNrw0digCV0e45mCgaQs2F0q4v2O8C
+xjl9pbsuf2qz1jHKGdJIXuhaW1XRqCOE2ZHc/n/+s2s8TUIcBve3B2glKxJhgyV8
+nDpZkjOEctef8uFPU3Alfm382kj0THcXdgsQ+jreLJ1VCS5xNcU6VpXa4QIDAQAB
+AoGAOHsl4tDB2TTvuKekgURK5ykdLt1dk0N0Hk7B5HJ4HrdUaSXeNYHWMMnc+PrF
+DdWTR3BD5yxKqpyUmBz5eQZyA8vVKzEVmYCkA+EO6TQeo6xveH/9xaFbTtXpwtvS
+N9m3kwEfmfudJvQRFb3q79I+17/g8rWbZlDYK7CKyfVs17UCQQDxdMOz/Q7xpP+f
+sXTHxvhtw4FFvAZEOEQA1a+uHGSmz+Vq0SIOpwZwri4aFG1YVUS2FUGHuhSpsuUJ
+Pg3kY1N3AkEAwGiiObgemFQLvCVigP8YcZyt98a+vE2Joq3iJyd/4DnEqvN98WNm
+5zaSDEXAJzC1ZuqnMUFVbiYBt2W4InBqZwJAEpgyZg8L8pIJWYv5+VSaVyGiN/OV
+6/UFT6clI1xuZ+ZEvagjXkuAlHbld/6wuQfABeG3LTOoWbU8LC0KNtdrWwJAF0gR
+6R4IRbJVwSxc4PL9CDJHMqYPykUvlEmqBcbXyE/1JiJUaPL4Lp4Byg5ek99m888M
+7/7R0YQzzPc38qLbnQJBAMbs/L0td6AponlpHCLmhHd7dka6GNIdyaALLNSVefD+
++MLQ7dATQne1y5n08vswMX9QnNTxFnlK59gWk/0gow4=
+-----END RSA PRIVATE KEY-----

Added: jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/id_rsa.pub
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp-Plugin-OAuth/t/id_rsa.pub	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,5 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIGJAoGBALV6QzjTakSJnozJobRHMM2vDR2KAJXR7jmYKBpCzYXSri/Y7wLGOX2l
+uy5/arPWMcoZ0khe6FpbVdGoI4TZkdz+f/6zazxNQhwG97cHaCUrEmGDJXycOlmS
+M4Ry15/y4U9TcCV+bfzaSPRMdxd2CxD6Ot4snVUJLnE1xTpWldrhAgMBAAE=
+-----END RSA PUBLIC KEY-----

Added: jifty/branches/virtual-models/t/TestApp/etc/config.yml
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/etc/config.yml	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,5 @@
+---
+application:
+    ThisConfigFile: etc/config.yml
+    EtcConfig: 1
+

Added: jifty/branches/virtual-models/t/TestApp/etc/site_config.yml
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/etc/site_config.yml	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,5 @@
+---
+application:
+    ThisConfigFile: etc/site_config.yml
+    EtcSiteConfig: 1
+

Added: jifty/branches/virtual-models/t/TestApp/lib/TestApp/Action/SayHi.pm
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/lib/TestApp/Action/SayHi.pm	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,18 @@
+package TestApp::Action::SayHi;
+
+use Jifty::Param::Schema;
+use Jifty::Action schema {
+
+    param 'name';
+    param 'greeting';
+
+
+};
+
+sub take_action {
+    my $self = shift;
+
+    $self->result->message($self->argument_value('name').', '. $self->argument_value('greeting'));
+}
+
+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 Oct 20 16:01:19 2007
@@ -33,7 +33,7 @@
   type is 'datetime',
   is immutable,
   default is defer { DateTime->now },
-  filters are 'Jifty::DBI::Filter::DateTime';
+  filters are qw(Jifty::Filter::DateTime Jifty::DBI::Filter::Date);
 column 'uuid' => is UUID;
 };
 

Modified: jifty/branches/virtual-models/t/TestApp/lib/TestApp/View.pm
==============================================================================
--- jifty/branches/virtual-models/t/TestApp/lib/TestApp/View.pm	(original)
+++ jifty/branches/virtual-models/t/TestApp/lib/TestApp/View.pm	Sat Oct 20 16:01:19 2007
@@ -6,6 +6,26 @@
 
 __PACKAGE__->use_mason_wrapper;
 
+template 'say_hi' => page {
+    my $a = Jifty->web->new_action( class => 'SayHi' );
+    form {
+
+        #render_param($a => 'name');
+        render_param( $a => 'greeting' );
+        Jifty->web->form->submit(
+            label   => _('Create'),
+            onclick => [
+                {   submit => {
+                        action    => $a,
+                        arguments => { name => 'dave' }
+                    }
+                }
+            ]
+        );
+
+    };
+};
+
 template 'concrete2.html' => sub {
     html {
         body {
@@ -110,4 +130,7 @@
     h1 { 'In a Mason Wrapper?' };
 };
 
+
+
+
 1;

Added: jifty/branches/virtual-models/t/TestApp/t/config/01-basic.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/t/config/01-basic.t	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,33 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use lib 't/lib';
+use Jifty::SubTest;
+
+use Jifty::Test;
+
+my %option_from_file = (
+    EtcConfig         => 'etc/config.yml',
+    EtcSiteConfig     => 'etc/site_config.yml',
+    TTestConfig       => 't/test_config.yml',
+    TConfigTestConfig => 't/config/test_config.yml',
+);
+
+my %no_option_from_file = (
+    IndividualFile    => 't/config/02-individual.t-config.yml',
+);
+
+plan tests => 2 + keys(%option_from_file) + keys %no_option_from_file;
+
+ok(Jifty->config->framework('Web')->{'Port'} >= 10000, "default test config still exists");
+is(Jifty->config->app('ThisConfigFile'), 't/config/test_config.yml', "the same value merges correctly");
+
+while (my ($option, $file) = each %option_from_file) {
+    is(Jifty->config->app($option), '1', "options from $file loaded");
+}
+
+while (my ($option, $file) = each %no_option_from_file) {
+    is(Jifty->config->app($option), undef, "options from $file NOT loaded");
+}
+

Added: jifty/branches/virtual-models/t/TestApp/t/config/02-individual.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/t/config/02-individual.t	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,26 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use lib 't/lib';
+use Jifty::SubTest;
+
+use Jifty::Test;
+
+my %option_from_file = (
+    EtcConfig         => 'etc/config.yml',
+    EtcSiteConfig     => 'etc/site_config.yml',
+    TTestConfig       => 't/test_config.yml',
+    TConfigTestConfig => 't/config/test_config.yml',
+    IndividualFile    => 't/config/02-individual.t-config.tml ',
+);
+
+plan tests => 2 + keys %option_from_file;
+
+ok(Jifty->config->framework('Web')->{'Port'} >= 10000, "default test config still exists");
+is(Jifty->config->app('ThisConfigFile'), 't/config/02-individual.t-config.yml', "the same value merges correctly");
+
+while (my ($option, $file) = each %option_from_file) {
+    is(Jifty->config->app($option), '1', "options from $file loaded");
+}
+

Added: jifty/branches/virtual-models/t/TestApp/t/config/02-individual.t-config.yml
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/t/config/02-individual.t-config.yml	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,5 @@
+---
+application:
+    ThisConfigFile: t/config/02-individual.t-config.yml
+    IndividualFile: 1
+

Added: jifty/branches/virtual-models/t/TestApp/t/config/03-nosubtest.t
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/t/config/03-nosubtest.t	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,30 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use Jifty::Test;
+
+my %option_from_file = (
+    TTestConfig       => 't/test_config.yml',
+    TConfigTestConfig => 't/config/test_config.yml',
+);
+
+my %no_option_from_file = (
+    EtcConfig         => 'etc/config.yml',
+    EtcSiteConfig     => 'etc/site_config.yml',
+    IndividualFile    => 't/config/02-individual.t-config.yml',
+);
+
+plan tests => 2 + keys(%option_from_file) + keys %no_option_from_file;
+
+ok(Jifty->config->framework('Web')->{'Port'} >= 10000, "default test config still exists");
+is(Jifty->config->app('ThisConfigFile'), 't/config/test_config.yml', "the same value merges correctly");
+
+while (my ($option, $file) = each %option_from_file) {
+    is(Jifty->config->app($option), '1', "options from $file loaded");
+}
+
+while (my ($option, $file) = each %no_option_from_file) {
+    is(Jifty->config->app($option), undef, "options from $file NOT loaded");
+}
+

Added: jifty/branches/virtual-models/t/TestApp/t/config/test_config.yml
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/t/config/test_config.yml	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,5 @@
+---
+application:
+    ThisConfigFile: t/config/test_config.yml
+    TConfigTestConfig: 1
+

Added: jifty/branches/virtual-models/t/TestApp/t/test_config.yml
==============================================================================
--- (empty file)
+++ jifty/branches/virtual-models/t/TestApp/t/test_config.yml	Sat Oct 20 16:01:19 2007
@@ -0,0 +1,5 @@
+---
+application:
+    ThisConfigFile: t/test_config.yml
+    TTestConfig: 1
+

Modified: jifty/branches/virtual-models/t/lib/Jifty/SubTest.pm
==============================================================================
--- jifty/branches/virtual-models/t/lib/Jifty/SubTest.pm	(original)
+++ jifty/branches/virtual-models/t/lib/Jifty/SubTest.pm	Sat Oct 20 16:01:19 2007
@@ -2,7 +2,11 @@
 
 use FindBin;
 use File::Spec;
+use Cwd;
+
 BEGIN {
+    $Jifty::SubTest::OrigCwd = Cwd::cwd;
+
     @INC = grep { defined } map { ref($_) ? $_ : File::Spec->rel2abs($_) } @INC;
     chdir "$FindBin::Bin/..";
 }


More information about the Jifty-commit mailing list