Compare commits

..
228 Commits
Author SHA1 Message Date
mdipierro c2b6c645ef R-2.1.1 2012-10-15 06:40:55 -05:00
mdipierro 36810aaccc fixed error for missing driver 2012-10-15 06:36:44 -05:00
mdipierro 8e14d1c92e updated changelog 2012-10-15 06:15:33 -05:00
mdipierro 9b7e944dea improved custom_inport with app-level track changes 2012-10-15 05:55:45 -05:00
mdipierro b6429ca5fd better errors in custom_import, thanks Michele 2012-10-15 05:46:57 -05:00
mdipierro 8607b09e64 -Y starts cron 2012-10-15 05:33:21 -05:00
mdipierro 64151a2c3e reverted custom_install tests 2012-10-14 20:07:54 -05:00
mdipierro d5749db0cd cron disabled by default unless -H 2012-10-14 18:29:17 -05:00
mdipierro df7d6847e6 fixed issue 1088 2012-10-14 16:40:49 -05:00
mdipierro c6b5ad8179 auth = Auth(db).define_tables() in one line 2012-10-14 15:55:45 -05:00
mdipierro 71ec500fde changed default driver for sqlite, thanks Niphlod 2012-10-14 15:32:33 -05:00
mdipierro 6cf05edf63 DAL(...,do_connect=False,migrate_enabled=False) 2012-10-14 15:08:25 -05:00
mdipierro 39c5341dbc auth_wiki extra fields, thanks Alan 2012-10-14 14:44:37 -05:00
mdipierro e2234a8771 added storage of length, notnull, unique in .table files 2012-10-14 14:41:28 -05:00
mdipierro cac9741cb7 wiki preview patch, thanks Niphlod 2012-10-14 14:28:17 -05:00
mdipierro 648cb296e6 bettere errors in custom_import, thanks Michele 2012-10-14 14:20:12 -05:00
mdipierro 5a0ebb7d8c faster better web2py_uuid, thanks Michele 2012-10-14 14:16:43 -05:00
mdipierro b7b534ccf2 new tests in test_web, tests static/_1.2.3/... 2012-10-13 14:36:20 -05:00
mdipierro fab23d210b removed un-necessary setup_exe_2.6.py 2012-10-13 10:16:06 -05:00
mdipierro 6a18539f7e no more custom_import_install in gaehandler.py 2012-10-13 09:37:41 -05:00
mdipierro 4d87a90c10 redis session patch, thanks Niphlod 2012-10-13 09:26:53 -05:00
mdipierro 2c9767e500 fixed issue 1081, conflict betweeen web2py css error and bootstrap error classes, thanks Paolo and lapcchan 2012-10-13 09:25:32 -05:00
mdipierro 2b1d9acba5 fixed issue 1084 2012-10-12 22:16:23 -05:00
mdipierro 8a2d18b63b ip address validator fixed 2012-10-12 18:17:06 -05:00
Massimo b59f9f896c fixed problem with renaming of the rewrite.thread to rewrite.THREAD_LOCAL 2012-10-12 12:19:08 -05:00
Massimo 496e6663fd some minor simplifications in rewrite.py 2012-10-12 11:48:25 -05:00
Massimo a4fbf9b6b4 improved speed of regex_url_in 2012-10-12 11:33:02 -05:00
mdipierro b8afce746b much better custom_import, smaller, faster, and finally works with shell 2012-10-11 22:03:20 -05:00
mdipierro 4c239eaf79 fixed cron 2012-10-11 16:48:07 -05:00
mdipierro 1d03233c83 just alignment of custom_import 2012-10-11 09:23:36 -05:00
mdipierro 5358ad271e faster init on gae and better gae_memcache 2012-10-11 08:27:58 -05:00
Massimo 2fa7d72b01 added comments in cache, thanks Michele 2012-10-10 15:16:21 -05:00
mdipierro f034d83c81 fixed problem with DAL on googlesql 2012-10-10 12:20:25 -05:00
mdipierro b3afa2ab57 fixed impval=0, thanks vp 2012-10-10 12:10:51 -05:00
mdipierro 36fc758690 increased admin (is_local) security, thanks Jonathan 2012-10-10 12:00:59 -05:00
mdipierro f764eb2653 rocket passes all pathoc tests without hang 2012-10-10 11:57:30 -05:00
mdipierro 63f56ddecc faster local_hosts lookup 2012-10-10 11:43:06 -05:00
mdipierro 0b2bab3f3d fixed issue 1077, thanks c.guimaraes 2012-10-10 11:25:25 -05:00
mdipierro 66de02929b fixed gae_memcache increment bug, issue 1079, thanks in4tunio 2012-10-10 11:21:01 -05:00
mdipierro 3c4a142f9a many improvements in cache, lazy cache.ram and cache.disk 2012-10-10 11:03:52 -05:00
mdipierro bf6f090428 many improvements in cache 2012-10-10 11:01:09 -05:00
mdipierro 59ab818f5b better toolbar, thanks Niphlod 2012-10-10 09:19:01 -05:00
mdipierro 77847daa77 auth.wiki preview, thanks Niphlod 2012-10-10 09:18:09 -05:00
Massimo 690d2df774 redis_sessions.py, thanks Niphlod 2012-10-09 14:36:34 -05:00
Massimo 643cc9c141 added jquery.mobile 1.2.0 2012-10-09 09:04:27 -05:00
mdipierro 562cb58342 some work on mongodb but references do not work 2012-10-08 22:09:25 -05:00
mdipierro 1c8ef29413 fixed time_expire typo in mamcache, thanks Bruno 2012-10-08 12:09:00 -05:00
mdipierro 8e67c551de fixed problem with close all instances when no instances 2012-10-08 11:03:21 -05:00
mdipierro 03e3324a12 removed rc1 2012-10-08 09:15:33 -05:00
mdipierro a3ba3f662a 2.1.0rc1 2012-10-07 22:45:38 -05:00
mdipierro 2b21678d48 clearing zombie list. Should not be necessary but what the heck 2012-10-07 13:31:02 -05:00
mdipierro 09f29257d2 fixed issue 1074, thanks mjmare 2012-10-07 10:48:37 -05:00
mdipierro 62be48decf fixed issue 1072 2012-10-07 10:13:20 -05:00
mdipierro 952a29d5da fixed routes example, issue 1073 2012-10-07 10:12:04 -05:00
mdipierro db9151b993 fixed routes example, issue 1073 2012-10-07 10:11:10 -05:00
mdipierro e1bb2b4556 db.define_table(...,redefine=True) 2012-10-07 01:10:08 -05:00
mdipierro 668658b7c9 zombie dal is here, should fix scheduler problems while retaining Row/Rows serialization 2012-10-07 01:07:49 -05:00
mdipierro 231a3e1278 Try the mobile interface link in site 2012-10-05 10:38:58 -05:00
mdipierro 41d234bf48 fixed mobile admin, and improved it 2012-10-05 10:24:14 -05:00
mdipierro f16c3a394f lazy tables re-appeared in appadmin 2012-10-05 09:01:51 -05:00
mdipierro 570538042e fixed IMAP reconnect 2012-10-05 07:25:21 -05:00
mdipierro 6a97bfe517 Auth use request.requires_https, thanks Yarin 2012-10-05 07:18:35 -05:00
mdipierro 7b6e5c5270 fixed issue 990 2012-10-04 21:52:04 -05:00
mdipierro 20deb56e33 minor indentation change 2012-10-04 21:45:25 -05:00
mdipierro a23a2c52c2 fixed url_in(request, environ)[1]['PATH_INFO'] 2012-10-04 20:39:00 -05:00
mdipierro 3f6fa941cb fixed issue 1067 2012-10-04 20:36:30 -05:00
mdipierro 28f914463b removed depredation working, thanks Niphlod 2012-10-04 20:17:12 -05:00
mdipierro f0c84b5230 scheduler non-writable field, thanks Niphlod 2012-10-04 20:10:55 -05:00
mdipierro dc51abe54c Auth(secure=True), thanks Yarin and Niphlod 2012-10-04 15:12:52 -05:00
mdipierro f501da74b7 fixed setup, thanks Mike 2012-10-04 15:06:58 -05:00
mdipierro c1d14a35ee fixed preblem with db singledon serialization in tickets 2012-10-04 11:53:04 -05:00
mdipierro 4f4a0318aa fixed wiki for missing body 2012-10-04 10:44:35 -05:00
mdipierro 54a1005af3 fixed problem with __new__ deprecated 2012-10-04 08:44:26 -05:00
mdipierro 7b4fac7a7f allow re-definition of tables 2012-10-04 08:35:39 -05:00
Massimo c80a6db604 better request.requires_https, thanks Yarin and Niphlod 2012-10-03 12:32:12 -05:00
Massimo 96247eda51 fixed some problem with grid 2012-10-03 11:58:50 -05:00
Massimo 433a02537c fixed issue 1064 2012-10-03 11:54:03 -05:00
Massimo bacff3453b fised issue 1063 2012-10-02 14:55:56 -05:00
Massimo b20e74c899 wiki uses contains instead of startswith, thanks David 2012-10-02 14:50:16 -05:00
mdipierro efa5ceb6de fixed issue 738 2012-10-01 21:16:42 -05:00
mdipierro f79660dd2a attempt to fix issue 981 2012-10-01 21:11:45 -05:00
mdipierro 606daabdd3 possibly fixed 1020 2012-10-01 20:52:02 -05:00
mdipierro fc1e4ad620 fixed DAL.__new__ and SQLFORM.factory 2012-10-01 19:15:37 -05:00
mdipierro 54dcf2ead9 more customizaiton of booleans 2012-10-01 17:07:06 -05:00
mdipierro 69cea23cd4 better generic.xml, thanks Derek 2012-10-01 15:54:35 -05:00
mdipierro 083b598c93 fixed issue 1031, thanks David 2012-10-01 14:49:42 -05:00
mdipierro 94f11501ed another rocket fix 2012-10-01 14:25:38 -05:00
mdipierro 9ac4a11db1 fixed textarea for long translation strings in admin 2012-10-01 12:30:02 -05:00
mdipierro a56cf59136 speedup in build-environment, thanks Michele 2012-10-01 12:01:35 -05:00
mdipierro 12cc81147c fixed rocket (removed handling of static files, faster parse headers, pathoc fault-tolerant) 2012-10-01 11:57:45 -05:00
mdipierro f70737ddc2 simplification in after_update 2012-09-30 16:50:57 -05:00
mdipierro 1a22497b64 self.connector 2012-09-30 14:45:47 -05:00
mdipierro 6da330a001 adapter have do_connect 2012-09-30 14:19:52 -05:00
mdipierro f6652e7530 made contrib.pbkdf2 optionional for utils 2012-09-30 13:47:49 -05:00
mdipierro 5148c6ec3a cleaner code in dal 2012-09-30 13:40:41 -05:00
mdipierro 954c79abcb fixed Storage copy 2012-09-30 13:20:05 -05:00
mdipierro 7ec3fe3bfe experimental DAL rewrite (work in progress) 2012-09-30 08:37:27 -05:00
mdipierro 9ce1cde6ca fixed issues with grid form and b.parent 2012-09-30 08:32:03 -05:00
mdipierro 672af73e96 fixed admin and new dal 2012-09-30 07:30:08 -05:00
mdipierro 67022578e3 fixed some bugs with singleton and remobed password from session.auth.user 2012-09-30 07:26:23 -05:00
mdipierro 65fe4c16fc fixed problem with order of lazyness in DAL serialization in session 2012-09-30 00:23:15 -05:00
mdipierro b6f9cd1863 login works fine with lazy DAL 2012-09-30 00:10:25 -05:00
mdipierro 4908a5e7be reverted DAL singleton until better understood 2012-09-29 23:47:30 -05:00
mdipierro 853c0b1a3a Better LazySet 2012-09-29 23:38:07 -05:00
mdipierro d0d8a4827e pickle.dumps(db(query).select() now always works and loads restore the full object (almost) 2012-09-29 23:20:37 -05:00
mdipierro 4745ce42f9 DAL is now a pickeable singleton 2012-09-29 22:33:18 -05:00
mdipierro bba13becd0 in dal renamed thread to thread_local 2012-09-29 20:59:50 -05:00
mdipierro 7712e005c4 MemcacheClient(time_expire=3600) 2012-09-29 20:20:03 -05:00
mdipierro 07c667f469 better rocket parsing, support for multiline headers 2012-09-29 18:39:53 -05:00
mdipierro 57ad1ec1cf made new web2py_uuid working on python 2.5 2012-09-29 18:35:09 -05:00
mdipierro 60a043479c yet faster web2py_uuid by caching urandom results 2012-09-29 17:56:43 -05:00
mdipierro 1a43e1671f more web2py_uuid speedup, thanks Michele 2012-09-29 17:16:35 -05:00
mdipierro cd955d3603 fixed issue 1046, thanks Houdini 2012-09-29 10:17:06 -05:00
mdipierro 492a0c51d1 fixed copy.copy(storage), thanks Corne 2012-09-29 09:55:57 -05:00
mdipierro dd4a4c281f prettidate works with future dates, thanks Sanjoy 2012-09-29 09:53:46 -05:00
mdipierro 41ec6da72c fixed issue 1044, thanks Paolo 2012-09-28 20:20:05 -05:00
mdipierro e18f38d4da static asset management, thanks Niphlod 2012-09-28 17:06:11 -05:00
Massimo bd27df8fa5 fixed issue 1042, thanks Dominic 2012-09-28 12:47:36 -05:00
Massimo 2f44c2de9b closed issue 995, thanks Peter 2012-09-28 12:38:03 -05:00
mdipierro ccf154a807 fixed issue 1040 2012-09-28 09:25:27 -05:00
mdipierro 837453dd6e faster web2py_uuid() thanks Michele 2012-09-28 09:21:03 -05:00
mdipierro 64bc60cdf0 fixed search in wiki again for firebird 2012-09-28 08:51:18 -05:00
mdipierro af258c334e fixed menu hoover issue 1036, thanks Paolo 2012-09-27 22:28:59 -05:00
mdipierro 627fff624a fixed many bugs in dal, thanks Mart Senecal 2012-09-27 22:15:56 -05:00
mdipierro bc8127f6de fixed problem with pypy 2012-09-27 22:09:23 -05:00
mdipierro 19bf43815d added better comments to address issue 1033 2012-09-27 21:45:19 -05:00
mdipierro 7709074d7c fixed field_parent references broken in 2.0.x, thanks Marin 2012-09-27 15:12:36 -05:00
mdipierro 4b73f249dd removed some un-necessary has_attr 2012-09-27 14:43:57 -05:00
mdipierro ae9e2c2be0 fixed add button in grid 2012-09-27 14:21:55 -05:00
mdipierro fd8edb5aa2 fixed issue 1032, thanks Carlos 2012-09-27 14:09:05 -05:00
mdipierro 82ab59c46a http 422, better tagcloud, possibly fixed firbird issue with tagcloud 2012-09-27 14:05:43 -05:00
mdipierro 74a63e98b5 possibly fixed issue 1031, thanks villas 2012-09-26 12:45:56 -05:00
Massimo 9e23b3dac5 db.get(,default=None), thanks Carlos 2012-09-25 16:18:52 -05:00
Massimo 077ce011e2 distinct instead of groupby 2012-09-25 14:56:49 -05:00
Massimo 1430bc824f test 2012-09-25 14:50:32 -05:00
mdipierro 722b16e620 CAS should disabled retrive username 2012-09-24 22:44:19 -05:00
mdipierro 30727ef9e4 synced web2py.js, thanks Jeremy 2012-09-24 20:18:30 -05:00
mdipierro 0f322f8a69 db().select(...query.case('true','false)) code cleanup 2012-09-24 18:09:22 -05:00
mdipierro ec5b4dde6f db().select(...query.case('true','false)) 2012-09-24 18:06:07 -05:00
mdipierro 630dcb799e gluon/contrib/comet_messaging.py -> gluon/contrib/websocket_messaging.py 2012-09-24 16:21:11 -05:00
mdipierro c6d1e47226 nore more typo fixed, thanks niphlod 2012-09-24 15:54:58 -05:00
mdipierro aa8315a7c9 data, no attr, thanks Niphlod 2012-09-24 15:13:34 -05:00
mdipierro 80040bf8e5 apos entity fix, thanks Michele 2012-09-24 12:48:39 -05:00
mdipierro 8f382f322e better setup-web2py-nginx-uwsgi-ubuntu.sh, thanks Bruno 2012-09-24 12:44:42 -05:00
mdipierro 9ccc7dc59a crud methods have FORM **attributes, thanks Daniel 2012-09-24 12:43:22 -05:00
mdipierro 70f6f86827 fixed https://github.com/web2py/web2py/pull/33, fetching Oracle Clobs, thanks dhx 2012-09-24 12:39:28 -05:00
mdipierro 3055c87567 fixed issue 1028, thanks Sherdim 2012-09-24 11:59:57 -05:00
mdipierro 473955611a addClass/hasClass entropy_check, thanks Niphlod 2012-09-24 11:50:59 -05:00
mdipierro 21a7ded8e0 fixed minify code to fix paths in css files, thanks Jeremy 2012-09-24 10:11:35 -05:00
mdipierro 23347995e2 css:inline patch,thanks Jeremy 2012-09-24 09:25:26 -05:00
mdipierro 4027a3cda3 SQLFORM().accepts(onvalidation=dict(onchange=lambda....)), thanks Alan 2012-09-23 19:24:05 -05:00
mdipierro a9b6c4f1a3 linked_tables in smartgrid can be a dict() 2012-09-23 19:16:27 -05:00
mdipierro c1330662aa better entropy colors and optional callback, thanks Niphlod 2012-09-23 17:00:34 -05:00
mdipierro 3e38f8017f better entropy colors and optional callback, thanks Niphlod 2012-09-23 16:58:11 -05:00
mdipierro 41bc1cb1e9 linked_tables in smartgrid can be a dict() 2012-09-23 16:31:44 -05:00
mdipierro 9fb94008ea linked_tables in smartgrid can be a dict() 2012-09-23 16:30:31 -05:00
mdipierro 5f647a46b8 moved layout.js in web2py_bootstrap.js and include_files(extensions=[...]), thanks Jeremy 2012-09-23 14:37:50 -05:00
mdipierro 69a2e76c3c entropy check code in web2py.js, thanks Niphlod 2012-09-23 14:07:50 -05:00
mdipierro 4d6598c645 fixed typo in validators 2012-09-22 11:19:14 -05:00
mdipierro 56acb685e9 removed some duplication of code in dal.py, thanks Mart 2012-09-22 10:11:17 -05:00
mdipierro f0be4416b3 restored comments in boostrap_css.css 2012-09-22 09:54:53 -05:00
mdipierro 93ce59f65b moved entropy js back to user.html 2012-09-22 09:52:08 -05:00
mdipierro 28b94eceb2 formstyle='inline' for forms with single field 2012-09-21 22:49:45 -05:00
mdipierro e4c63769f2 reverted 4077 because distinct on and groupby are not equivalent. Moreover distinct on is supported only by postgresql because potentially non-deterministic and should be discouraged. Thanks Alexander 2012-09-21 22:29:55 -05:00
mdipierro e0fc61932d broken long lines in minify 2012-09-21 22:23:45 -05:00
mdipierro 3de3046260 fixed share Google+, thanks nicozanf 2012-09-21 22:14:08 -05:00
mdipierro b6a96301cd Merge branch 'master' of https://github.com/nicozanf/web2py 2012-09-21 22:13:09 -05:00
mdipierro 480dea23ad moved entropy JS in PasswordWidget but the source should be moved in separate file 2012-09-21 22:12:17 -05:00
mdipierro 457ef24c04 added missing patch, thanks Niphlod 2012-09-21 22:01:51 -05:00
Nico Zanferrari dbf6ce6d6c Modified share to Buzz to Google+ 2012-09-22 00:10:23 +02:00
nicozanf 7686a6fc93 Merge pull request #3 from mdipierro/master
up
2012-09-21 14:32:55 -07:00
mdipierro afde6efafa cleanup web2py_bootstrap.css 2012-09-21 09:37:37 -05:00
mdipierro af53f17f5a scheduler optimization for single process, thanks Niphlod 2012-09-21 08:58:43 -05:00
mdipierro 8f9cf4034d user.html has entrpy color-coding, thanks Niphlod 2012-09-21 08:57:11 -05:00
mdipierro 31a6c5df9b hidden fields should be visible 2012-09-21 08:29:16 -05:00
nicozanf 08d1b26738 Merge pull request #1 from mdipierro/master
test
2012-09-20 13:29:35 -07:00
mdipierro abd739123c added search to codemirror 2012-09-20 13:25:54 -05:00
mdipierro da8570b560 better zip_static_files.py, thanks Niphlod 2012-09-20 11:19:11 -05:00
mdipierro 8d238890a2 fixed issue 1021, thanks Edouard and Jonathan 2012-09-20 11:16:51 -05:00
nicozanf ca29f262e5 Unnecessary calls to logging()
the module logging() is not imported and called unnecessary.
2012-09-20 17:52:55 +03:00
mdipierro cffefbeb2e possibly fixed issue 1020 2012-09-20 09:27:05 -05:00
mdipierro 52528f9311 changed brand color 2012-09-20 09:24:38 -05:00
mdipierro 8b75256281 better layout yet, thanks Paolo 2012-09-20 09:19:21 -05:00
mdipierro 2385ed22b1 new web2py_bootstrap_nojs.css, thanks Paolo 2012-09-20 09:16:04 -05:00
mdipierro 7a0cf446ba entropy computation in IS_STRONG, thanks Jonathan 2012-09-20 09:14:51 -05:00
mdipierro 586d85cf08 fixed a problem with reset_password redirect loop 2012-09-19 23:04:10 -05:00
mdipierro f371e72e07 mail.send(...,sender='Mr X <%(sender)s>') 2012-09-19 00:03:06 -05:00
mdipierro 8d9bc1cb53 mail.send(...,sender='Mr X <%(sender)s>') 2012-09-19 00:00:18 -05:00
mdipierro 29edf94498 enhanced imapadapter, thanks Alan 2012-09-18 13:39:16 -05:00
mdipierro d7a108ff06 fixed recent typo, thanks Christian 2012-09-18 13:37:41 -05:00
mdipierro 5ed41285c0 possibly fixed issue 1016 2012-09-18 13:35:47 -05:00
mdipierro b16d79e654 some more CSS fixes, thanks Paolo 2012-09-17 21:37:13 -05:00
mdipierro 90e5d06d3d bootstrap 2.1.1, thanks Paolo 2012-09-17 21:32:49 -05:00
mdipierro a1156cae6c improved minify, thanks Niphlod 2012-09-17 21:25:05 -05:00
mdipierro 037a200357 IS_IN_DB cacheable=True 2012-09-17 16:22:30 -05:00
mdipierro e282454bfe fixed a problem with order of table definitions in auth.wiki, thanks David 2012-09-17 14:23:13 -05:00
mdipierro e499c1d5a4 fixed problem with missing wiki_media.filename 2012-09-17 11:47:07 -05:00
mdipierro 8540b1b113 fixed issue 1014, thanks Sherdim 2012-09-17 11:43:26 -05:00
mdipierro 6b5e3f2abf better layout, thanks Paolo 2012-09-17 11:36:00 -05:00
mdipierro f8be550e6a fixed comment 2012-09-17 10:26:17 -05:00
mdipierro 6a7b04199f fixed issue 1005, thanks howesc 2012-09-17 09:36:13 -05:00
mdipierro 2847e0f4df fixed imap dal __contains__ issue 1013, thanks Alan 2012-09-17 09:12:13 -05:00
mdipierro eec7f7c687 cid argument in SQLTABLE, thanks Massimiliano 2012-09-17 08:58:01 -05:00
mdipierro a4253deda3 fixed ducktyping problem, thanks Jonathan 2012-09-17 07:13:26 -05:00
mdipierro 372d639601 fixed exception on missin stored_password 2012-09-17 07:10:51 -05:00
mdipierro 06e5c6869d scripts/zip_static_files.py, thanks Niphlod 2012-09-16 15:55:10 -05:00
mdipierro 933189c216 niphlod is right, no need to to restrict gzip streaming to rocket 2012-09-16 15:54:20 -05:00
mdipierro c942881f36 convert distinct to groupby when not supported 2012-09-16 15:51:51 -05:00
mdipierro 99e491ca0f better streamer 2012-09-16 15:38:31 -05:00
mdipierro ea539d50e2 adding missing import in scheduler 2012-09-16 14:10:39 -05:00
mdipierro 497bbf002c possibly fixed issue 1011 2012-09-16 13:55:29 -05:00
mdipierro 2629fac624 fixed problem with error on liststring widget 2012-09-16 13:06:43 -05:00
mdipierro 30af5901eb fixed alignment in forms with list:string 2012-09-16 12:23:19 -05:00
mdipierro 42b60d3291 fixed datetime T in scheduler 2012-09-16 11:46:03 -05:00
mdipierro 9b53fd2302 fixed issue 1010, error reporting on list:string 2012-09-16 11:43:03 -05:00
mdipierro 972f64b6f9 better impersonate, thanks Alan 2012-09-16 10:43:39 -05:00
mdipierro 3eb4627ccf typo in new appadmin.html 2012-09-16 08:53:01 -05:00
mdipierro 2ba88b8951 auth.wiki(render='markmin'), thanks Guruyaya 2012-09-15 15:42:44 -05:00
mdipierro 117b04169c wiki file should be filename, thanks David 2012-09-15 13:41:24 -05:00
mdipierro c56d5bd066 gae_memcache.clear(), thanks Matt 2012-09-14 21:17:01 -05:00
mdipierro 00bfde9ce9 cache.with_prefix(cache.ram,'prefix'), thanks Bruno 2012-09-14 16:08:25 -05:00
mdipierro 5f503d0427 db.t.f.epoch(), thanks Niphlod 2012-09-14 15:51:58 -05:00
mdipierro 47bcab6b26 fixed google+ share 2012-09-14 09:42:16 -05:00
mdipierro ecca0439f1 db(table).select() uses table._id<>None instead of table._id>0 2012-09-14 09:27:09 -05:00
mdipierro e896be4037 fixed typo in setup script, thanks Devon 2012-09-14 09:23:31 -05:00
mdipierro b0e4284130 uninstalled apps backup under deposit 2012-09-14 08:47:42 -05:00
mdipierro 3e69889cca removed troublesome chipin widget 2012-09-13 21:48:40 -05:00
mdipierro dfb2e62c6c tables and field names should be allowed to start in a number, thanks Adi 2012-09-13 21:04:45 -05:00
105 changed files with 3078 additions and 2672 deletions
+34 -1
View File
@@ -1,4 +1,37 @@
## 2.00.1 ## 2.1.0
- overall faster web2py
- when apps are deleted, a w2p copy left in deposit folder
- change in cron (it is now disabled by default). removed -N option and introduced -Y.
- faster web2py_uuid() and request initialization logic, thanks Michele
- static asset management, thanks Niphlod
- improved mobile admin
- request.requires_https and Auth(secure=True), thanks Yarin and Niphlod
- better custom_import (works per app and is faster), thanks Michele
- redis_sesssion.py, thanks Niphlod
- allow entropy computation in IS_STRONG and web2py.js, thanks Jonathan and Niphlod
- fixed many aith.wiki problems
- support for auth.wiki(render='html')
- better welcome layout, thanks Paolo
- db.define_table(...,redefine=True)
- DAL, Row, and Rows object can now be pickled/unpickled, thanks to zombie DAL.
- admin uses codemirror
- allow syntax auth = Auth(db).define_tables()
- better auth.wiki with preview, thanks Alan
- better auth.impersonate, thanks Alan
- upgraded jQuery 1.8
- upgraded Bootstrap 2.1
- fixed problem with dropbox_account.py
- many fixes to cache.ram, cache.disk, memcache and gae_memcache
- cache.with_prefix(cache.ram,'prefix')
- db.table.field.epoch() counts seconds from epoch
- DAL support for SQL CASE, example: db().select(...query.case('true','false))
- DAL(...,do_connect=False) allows faking connections
- DAL(...,auto_import=True) now retieves some fiel attributes
- mail can specify a sender: mail.send(...,sender='Mr X <%(sender)s>')
- renamed gluon/contrib/comet_messaging.py -> gluon/contrib/websocket_messaging.py
## 2.0.1-11
### DAL Improvements ### DAL Improvements
+8 -5
View File
@@ -29,7 +29,7 @@ update:
wget -O gluon/contrib/simplejsonrpc.py http://rad2py.googlecode.com/hg/ide2py/simplejsonrpc.py wget -O gluon/contrib/simplejsonrpc.py http://rad2py.googlecode.com/hg/ide2py/simplejsonrpc.py
echo "remember that pymysql was tweaked" echo "remember that pymysql was tweaked"
src: src:
echo 'Version 2.0.9 ('`date +%Y-%m-%d\ %H:%M:%S`') stable' > VERSION echo 'Version 2.1.1 ('`date +%Y-%m-%d\ %H:%M:%S`') stable' > VERSION
### rm -f all junk files ### rm -f all junk files
make clean make clean
### clean up baisc apps ### clean up baisc apps
@@ -107,10 +107,6 @@ win:
cp applications/__init__.py ../web2py_win/web2py/applications cp applications/__init__.py ../web2py_win/web2py/applications
cd ../web2py_win; zip -r web2py_win.zip web2py cd ../web2py_win; zip -r web2py_win.zip web2py
mv ../web2py_win/web2py_win.zip . mv ../web2py_win/web2py_win.zip .
pip:
# create Web2py distribution for upload to Pypi
# after upload clean Web2py sources with rm -R ./dist
python setup.py sdist
run: run:
python2.5 web2py.py -a hello python2.5 web2py.py -a hello
commit: commit:
@@ -129,3 +125,10 @@ tag:
hg tag -l '$(S)' hg tag -l '$(S)'
make commit S='$(S)' make commit S='$(S)'
make push make push
pip:
# create Web2py distribution for upload to Pypi
# after upload clean Web2py sources with rm -R ./dist
# http://guide.python-distribute.org/creation.html
python setup.py sdist
python setup.py register
python setup.py sdist upload
+1 -1
View File
@@ -1 +1 @@
Version 2.0.9 (2012-09-13 17:48:15) stable Version 2.1.1 (2012-10-15 06:40:52) stable
-1
View File
@@ -80,7 +80,6 @@ skip_files: |
builtins: builtins:
- remote_api: on - remote_api: on
# - datastore_admin: on
- appstats: on - appstats: on
- admin_redirect: on - admin_redirect: on
- deferred: on - deferred: on
+4 -1
View File
@@ -113,7 +113,6 @@ def query_by_table_type(tablename,db,request=request):
# ## list all databases and tables # ## list all databases and tables
# ########################################################### # ###########################################################
def index(): def index():
return dict(databases=databases) return dict(databases=databases)
@@ -207,6 +206,7 @@ def select():
if match: if match:
table = match.group('table') table = match.group('table')
try: try:
tb = None
nrows = db(query).count() nrows = db(query).count()
if form.vars.update_check and form.vars.update_fields: if form.vars.update_check and form.vars.update_fields:
db(query).update(**eval_in_global_env('dict(%s)' db(query).update(**eval_in_global_env('dict(%s)'
@@ -221,6 +221,8 @@ def select():
else: else:
rows = db(query,ignore_common_filters=True).select(limitby=(start, stop)) rows = db(query,ignore_common_filters=True).select(limitby=(start, stop))
except Exception, e: except Exception, e:
import traceback
tb = traceback.format_exc()
(rows, nrows) = ([], 0) (rows, nrows) = ([], 0)
response.flash = DIV(T('Invalid Query'),PRE(str(e))) response.flash = DIV(T('Invalid Query'),PRE(str(e)))
# begin handle upload csv # begin handle upload csv
@@ -250,6 +252,7 @@ def select():
rows=rows, rows=rows,
query=request.vars.query, query=request.vars.query,
formcsv = formcsv, formcsv = formcsv,
tb = tb,
) )
+9 -9
View File
@@ -3,7 +3,6 @@
EXPERIMENTAL_STUFF = True EXPERIMENTAL_STUFF = True
if EXPERIMENTAL_STUFF: if EXPERIMENTAL_STUFF:
is_mobile = request.user_agent().is_mobile
if is_mobile: if is_mobile:
response.view = response.view.replace('default/','default.mobile/') response.view = response.view.replace('default/','default.mobile/')
response.menu = [] response.menu = []
@@ -366,10 +365,15 @@ def uninstall():
else: else:
session.flash = T('no permission to uninstall "%s"', app) session.flash = T('no permission to uninstall "%s"', app)
redirect(URL('site')) redirect(URL('site'))
if app_uninstall(app, request): try:
session.flash = T('application "%s" uninstalled', app) filename = app_pack(app, request, raise_ex=True)
else: except:
session.flash = T('unable to uninstall "%s"', app) session.flash = T('unable to uninstall "%s"', app)
else:
if app_uninstall(app, request):
session.flash = T('application "%s" uninstalled', app)
else:
session.flash = T('unable to uninstall "%s"', app)
redirect(URL('site')) redirect(URL('site'))
return dict(app=app, dialog=dialog) return dict(app=app, dialog=dialog)
@@ -782,7 +786,7 @@ def edit_language():
_class='untranslated' if k==s else 'translated' _class='untranslated' if k==s else 'translated'
if len(key) <= 40: if len(s) <= 40:
elem = INPUT(_type='text', _name=name, value=s, elem = INPUT(_type='text', _name=name, value=s,
_size=70,_class=_class) _size=70,_class=_class)
else: else:
@@ -1647,7 +1651,6 @@ def git_pull():
session.flash = T("Application updated via git pull") session.flash = T("Application updated via git pull")
redirect(URL('site')) redirect(URL('site'))
except CheckoutError, message: except CheckoutError, message:
logging.error(message)
session.flash = T("Pull failed, certain files could not be checked out. Check logs for details.") session.flash = T("Pull failed, certain files could not be checked out. Check logs for details.")
redirect(URL('site')) redirect(URL('site'))
except UnmergedEntriesError: except UnmergedEntriesError:
@@ -1657,11 +1660,9 @@ def git_pull():
session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.") session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.")
redirect(URL('site')) redirect(URL('site'))
except GitCommandError, status: except GitCommandError, status:
logging.error(str(status))
session.flash = T("Pull failed, git exited abnormally. See logs for details.") session.flash = T("Pull failed, git exited abnormally. See logs for details.")
redirect(URL('site')) redirect(URL('site'))
except Exception,e: except Exception,e:
logging.error("Unexpected error:", sys.exc_info()[0])
session.flash = T("Pull failed, git exited abnormally. See logs for details.") session.flash = T("Pull failed, git exited abnormally. See logs for details.")
redirect(URL('site')) redirect(URL('site'))
elif 'cancel' in request.vars: elif 'cancel' in request.vars:
@@ -1693,7 +1694,6 @@ def git_push():
session.flash = T("Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.") session.flash = T("Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.")
redirect(URL('site')) redirect(URL('site'))
except Exception, e: except Exception, e:
logging.error("Unexpected error:", sys.exc_info()[0])
session.flash = T("Push failed, git exited abnormally. See logs for details.") session.flash = T("Push failed, git exited abnormally. See logs for details.")
redirect(URL('site')) redirect(URL('site'))
return dict(app=app,form=form) return dict(app=app,form=form)
+1 -1
View File
@@ -39,7 +39,7 @@ def deploy():
form = SQLFORM.factory( form = SQLFORM.factory(
Field('appcfg',default=GAE_APPCFG,label=T('Path to appcfg.py'), Field('appcfg',default=GAE_APPCFG,label=T('Path to appcfg.py'),
requires=EXISTS(error_message=T('file not found'))), requires=EXISTS(error_message=T('file not found'))),
Field('google_application_id',requires=IS_ALPHANUMERIC(),label=T('Google Application Id')), Field('google_application_id',requires=IS_MATCH('[\w\-]+'),label=T('Google Application Id')),
Field('applications','list:string', Field('applications','list:string',
requires=IS_IN_SET(apps,multiple=True), requires=IS_IN_SET(apps,multiple=True),
label=T('web2py apps to deploy')), label=T('web2py apps to deploy')),
+183 -125
View File
@@ -3,143 +3,26 @@
'!langcode!': 'it', '!langcode!': 'it',
'!langname!': 'Italiano', '!langname!': 'Italiano',
'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" è un\'espressione opzionale come "campo1=\'nuovo valore\'". Non si può fare "update" o "delete" dei risultati di un JOIN ', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" è un\'espressione opzionale come "campo1=\'nuovo valore\'". Non si può fare "update" o "delete" dei risultati di un JOIN ',
'%Y-%m-%d': '%d/%m/%Y',
'%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S',
'%s %%{row} deleted': '%s righe ("record") cancellate', '%s %%{row} deleted': '%s righe ("record") cancellate',
'%s %%{row} updated': '%s righe ("record") modificate', '%s %%{row} updated': '%s righe ("record") modificate',
'%Y-%m-%d': '%d/%m/%Y',
'%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S',
'(requires internet access)': '(requires internet access)', '(requires internet access)': '(requires internet access)',
'(something like "it-it")': '(qualcosa simile a "it-it")', '(something like "it-it")': '(qualcosa simile a "it-it")',
'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(file **gluon/contrib/plural_rules/%s.py** is not found)',
'@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files', '@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files',
'A new version of web2py is available: %s': 'È disponibile una nuova versione di web2py: %s', 'A new version of web2py is available: %s': 'È disponibile una nuova versione di web2py: %s',
'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': "ATTENZIONE: L'accesso richiede una connessione sicura (HTTPS) o l'esecuzione di web2py in locale (connessione su localhost)",
'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTZIONE: NON ESEGUIRE PIÙ TEST IN PARALLELO (I TEST NON SONO "THREAD SAFE")',
'ATTENTION: you cannot edit the running application!': "ATTENZIONE: non puoi modificare l'applicazione correntemente in uso ",
'About': 'informazioni', 'About': 'informazioni',
'About application': "Informazioni sull'applicazione", 'About application': "Informazioni sull'applicazione",
'Admin is disabled because insecure channel': 'amministrazione disabilitata: comunicazione non sicura',
'Admin language': 'Admin language',
'Administrator Password:': 'Password Amministratore:',
'Application name:': 'Application name:',
'Are you sure you want to delete file "%s"?': 'Confermi di voler cancellare il file "%s"?',
'Are you sure you want to delete plugin "%s"?': 'Confermi di voler cancellare il plugin "%s"?',
'Are you sure you want to uninstall application "%s"?': 'Confermi di voler disinstallare l\'applicazione "%s"?',
'Are you sure you want to upgrade web2py now?': 'Confermi di voler aggiornare web2py ora?',
'Available databases and tables': 'Database e tabelle disponibili',
'Cannot be empty': 'Non può essere vuoto',
'Cannot compile: there are errors in your app:': "Compilazione fallita: ci sono errori nell'applicazione.",
'Change admin password': 'change admin password',
'Check for upgrades': 'check for upgrades',
'Check to delete': 'Seleziona per cancellare',
'Checking for upgrades...': 'Controllo aggiornamenti in corso...',
'Clean': 'pulisci',
'Compile': 'compila',
'Controller': 'Controller',
'Controllers': 'Controllers',
'Copyright': 'Copyright',
'Create': 'crea',
'Create new simple application': 'Crea nuova applicazione',
'Current request': 'Richiesta (request) corrente',
'Current response': 'Risposta (response) corrente',
'Current session': 'Sessione (session) corrente',
'DB Model': 'Modello di DB',
'Database': 'Database',
'Date and Time': 'Data and Ora',
'Delete': 'Cancella',
'Delete:': 'Cancella:',
'Deploy': 'deploy',
'Deploy on Google App Engine': 'Installa su Google App Engine',
'EDIT': 'MODIFICA',
'Edit': 'modifica',
'Edit This App': 'Modifica questa applicazione',
'Edit application': 'Modifica applicazione',
'Edit current record': 'Modifica record corrente',
'Editing Language file': 'Modifica file linguaggio',
'Editing file "%s"': 'Modifica del file "%s"',
'Enterprise Web Framework': 'Enterprise Web Framework',
'Error logs for "%(app)s"': 'Log degli errori per "%(app)s"',
'Errors': 'errori',
'Exception instance attributes': 'Exception instance attributes',
'Functions with no doctests will result in [passed] tests.': 'I test delle funzioni senza "doctests" risulteranno sempre [passed].',
'Hello World': 'Salve Mondo',
'Help': 'aiuto',
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.',
'Import/Export': 'Importa/Esporta',
'Index': 'Indice',
'Install': 'installa',
'Installed applications': 'Applicazioni installate',
'Internal State': 'Stato interno',
'Invalid Query': 'Richiesta (query) non valida',
'Invalid action': 'Azione non valida',
'Language files (static strings) updated': 'Linguaggi (documenti con stringhe statiche) aggiornati',
'Languages': 'Linguaggi',
'Last saved on:': 'Ultimo salvataggio:',
'Layout': 'Layout',
'License for': 'Licenza relativa a',
'Login': 'Accesso',
'Login to the Administrative Interface': "Accesso all'interfaccia amministrativa",
'Logout': 'uscita',
'Main Menu': 'Menu principale',
'Menu Model': 'Menu Modelli',
'Models': 'Modelli',
'Modules': 'Moduli',
'NO': 'NO',
'New Record': 'Nuovo elemento (record)',
'New application wizard': 'New application wizard',
'New simple application': 'New simple application',
'No databases in this application': 'Nessun database presente in questa applicazione',
'Original/Translation': 'Originale/Traduzione',
'Overwrite installed app': 'sovrascrivi applicazione installata',
'PAM authenticated user, cannot change password here': 'utente autenticato tramite PAM, impossibile modificare password qui',
'Pack all': 'crea pacchetto',
'Pack compiled': 'crea pacchetto del codice compilato',
'Peeking at file': 'Uno sguardo al file',
'Plugin "%s" in application': 'Plugin "%s" nell\'applicazione',
'Plugins': 'I Plugins',
'Powered by': 'Powered by',
'Query:': 'Richiesta (query):',
'Remove compiled': 'rimozione codice compilato',
'Resolve Conflict file': 'File di risoluzione conflitto',
'Rows in table': 'Righe nella tabella',
'Rows selected': 'Righe selezionate',
'Saved file hash:': 'Hash del file salvato:',
'Site': 'sito',
'Start wizard': 'start wizard',
'Static files': 'Files statici',
'Stylesheet': 'Foglio di stile (stylesheet)',
'Sure you want to delete this object?': 'Vuoi veramente cancellare questo oggetto?',
'TM': 'TM',
'Testing application': 'Test applicazione in corsg',
'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La richiesta (query) è una condizione come ad esempio "db.tabella1.campo1==\'valore\'". Una condizione come "db.tabella1.campo1==db.tabella2.campo2" produce un "JOIN" SQL.',
'There are no controllers': 'Non ci sono controller',
'There are no models': 'Non ci sono modelli',
'There are no modules': 'Non ci sono moduli',
'There are no static files': 'Non ci sono file statici',
'There are no translators, only default language is supported': 'Non ci sono traduzioni, viene solo supportato il linguaggio di base',
'There are no views': 'Non ci sono viste ("view")',
'This is the %(filename)s template': 'Questo è il template %(filename)s',
'Ticket': 'Ticket',
'To create a plugin, name a file/folder plugin_[name]': 'Per creare un plugin, chiamare un file o cartella plugin_[nome]',
'Unable to check for upgrades': 'Impossibile controllare presenza di aggiornamenti',
'Unable to download app because:': 'Impossibile scaricare applicazione perché',
'Unable to download because': 'Impossibile scaricare perché',
'Unable to download because:': 'Unable to download because:',
'Uninstall': 'disinstalla',
'Update:': 'Aggiorna:',
'Upload & install packed application': 'Carica ed installa pacchetto con applicazione',
'Upload a package:': 'Upload a package:',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Per costruire richieste (query) più complesse si usano (...)&(...) come "e" (AND), (...)|(...) come "o" (OR), e ~(...) come negazione (NOT).',
'Use an url:': 'Use an url:',
'Version': 'Versione',
'View': 'Vista',
'Views': 'viste',
'Welcome %s': 'Benvenuto %s',
'Welcome to web2py': 'Benvenuto su web2py',
'YES': 'SI',
'additional code for your application': 'righe di codice aggiuntive per la tua applicazione', 'additional code for your application': 'righe di codice aggiuntive per la tua applicazione',
'Additional code for your application': 'Additional code for your application',
'admin disabled because no admin password': 'amministrazione disabilitata per mancanza di password amministrativa', 'admin disabled because no admin password': 'amministrazione disabilitata per mancanza di password amministrativa',
'admin disabled because not supported on google app engine': 'amministrazione non supportata da Google Apps Engine', 'admin disabled because not supported on google app engine': 'amministrazione non supportata da Google Apps Engine',
'admin disabled because unable to access password file': 'amministrazione disabilitata per impossibilità di leggere il file delle password', 'admin disabled because unable to access password file': 'amministrazione disabilitata per impossibilità di leggere il file delle password',
'Admin is disabled because insecure channel': 'amministrazione disabilitata: comunicazione non sicura',
'Admin language': 'Admin language',
'administrative interface': 'administrative interface', 'administrative interface': 'administrative interface',
'Administrator Password:': 'Password Amministratore:',
'and rename it (required):': 'e rinominala (obbligatorio):', 'and rename it (required):': 'e rinominala (obbligatorio):',
'and rename it:': 'e rinominala:', 'and rename it:': 'e rinominala:',
'appadmin': 'appadmin ', 'appadmin': 'appadmin ',
@@ -147,44 +30,104 @@
'application "%s" uninstalled': 'applicazione "%s" disinstallata', 'application "%s" uninstalled': 'applicazione "%s" disinstallata',
'application compiled': 'applicazione compilata', 'application compiled': 'applicazione compilata',
'application is compiled and cannot be designed': "l'applicazione è compilata e non si può modificare", 'application is compiled and cannot be designed': "l'applicazione è compilata e non si può modificare",
'Application name:': 'Application name:',
'are not used': 'are not used',
'are not used yet': 'are not used yet',
'Are you sure you want to delete file "%s"?': 'Confermi di voler cancellare il file "%s"?',
'Are you sure you want to delete plugin "%s"?': 'Confermi di voler cancellare il plugin "%s"?',
'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?',
'Are you sure you want to uninstall application "%s"?': 'Confermi di voler disinstallare l\'applicazione "%s"?',
'Are you sure you want to upgrade web2py now?': 'Confermi di voler aggiornare web2py ora?',
'arguments': 'arguments', 'arguments': 'arguments',
'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': "ATTENZIONE: L'accesso richiede una connessione sicura (HTTPS) o l'esecuzione di web2py in locale (connessione su localhost)",
'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTZIONE: NON ESEGUIRE PIÙ TEST IN PARALLELO (I TEST NON SONO "THREAD SAFE")',
'ATTENTION: you cannot edit the running application!': "ATTENZIONE: non puoi modificare l'applicazione correntemente in uso ",
'Available databases and tables': 'Database e tabelle disponibili',
'back': 'indietro', 'back': 'indietro',
'cache': 'cache', 'cache': 'cache',
'cache, errors and sessions cleaned': 'pulitura cache, errori and sessioni ', 'cache, errors and sessions cleaned': 'pulitura cache, errori and sessioni ',
'can be a git repo': 'can be a git repo',
'Cannot be empty': 'Non può essere vuoto',
'Cannot compile: there are errors in your app:': "Compilazione fallita: ci sono errori nell'applicazione.",
'cannot create file': 'impossibile creare il file', 'cannot create file': 'impossibile creare il file',
'cannot upload file "%(filename)s"': 'impossibile caricare il file "%(filename)s"', 'cannot upload file "%(filename)s"': 'impossibile caricare il file "%(filename)s"',
'Change admin password': 'change admin password',
'change password': 'cambia password', 'change password': 'cambia password',
'check all': 'controlla tutto', 'check all': 'controlla tutto',
'Check for upgrades': 'check for upgrades',
'Check to delete': 'Seleziona per cancellare',
'Checking for upgrades...': 'Controllo aggiornamenti in corso...',
'Clean': 'pulisci',
'click here for online examples': 'clicca per vedere gli esempi', 'click here for online examples': 'clicca per vedere gli esempi',
'click here for the administrative interface': "clicca per l'interfaccia amministrativa", 'click here for the administrative interface': "clicca per l'interfaccia amministrativa",
'click to check for upgrades': 'clicca per controllare presenza di aggiornamenti', 'click to check for upgrades': 'clicca per controllare presenza di aggiornamenti',
'code': 'code', 'code': 'code',
'collapse/expand all': 'collapse/expand all',
'Compile': 'compila',
'compiled application removed': "rimosso il codice compilato dell'applicazione", 'compiled application removed': "rimosso il codice compilato dell'applicazione",
'Controller': 'Controller',
'Controllers': 'Controllers',
'controllers': 'controllers', 'controllers': 'controllers',
'Copyright': 'Copyright',
'Create': 'crea',
'create file with filename:': 'crea un file col nome:', 'create file with filename:': 'crea un file col nome:',
'create new application:': 'create new application:', 'create new application:': 'create new application:',
'Create new simple application': 'Crea nuova applicazione',
'created by': 'creato da', 'created by': 'creato da',
'crontab': 'crontab', 'crontab': 'crontab',
'Current request': 'Richiesta (request) corrente',
'Current response': 'Risposta (response) corrente',
'Current session': 'Sessione (session) corrente',
'currently running': 'currently running', 'currently running': 'currently running',
'currently saved or': 'attualmente salvato o', 'currently saved or': 'attualmente salvato o',
'customize me!': 'Personalizzami!', 'customize me!': 'Personalizzami!',
'data uploaded': 'dati caricati', 'data uploaded': 'dati caricati',
'Database': 'Database',
'database': 'database', 'database': 'database',
'database %s select': 'database %s select', 'database %s select': 'database %s select',
'database administration': 'amministrazione database', 'database administration': 'amministrazione database',
'Date and Time': 'Data and Ora',
'db': 'db', 'db': 'db',
'DB Model': 'Modello di DB',
'Debug': 'Debug',
'defines tables': 'defininisce le tabelle', 'defines tables': 'defininisce le tabelle',
'Delete': 'Cancella',
'delete': 'Cancella', 'delete': 'Cancella',
'delete all checked': 'cancella tutti i selezionati', 'delete all checked': 'cancella tutti i selezionati',
'delete plugin': 'cancella plugin', 'delete plugin': 'cancella plugin',
'Delete this file (you will be asked to confirm deletion)': 'Delete this file (you will be asked to confirm deletion)',
'Delete:': 'Cancella:',
'Deploy': 'deploy',
'Deploy on Google App Engine': 'Installa su Google App Engine',
'Deploy to OpenShift': 'Deploy to OpenShift',
'design': 'progetta', 'design': 'progetta',
'Detailed traceback description': 'Detailed traceback description',
'direction: ltr': 'direction: ltr', 'direction: ltr': 'direction: ltr',
'Disable': 'Disable',
'docs': 'docs',
'done!': 'fatto!', 'done!': 'fatto!',
'download layouts': 'download layouts',
'download plugins': 'download plugins',
'EDIT': 'MODIFICA',
'Edit': 'modifica',
'Edit application': 'Modifica applicazione',
'edit controller': 'modifica controller', 'edit controller': 'modifica controller',
'Edit current record': 'Modifica record corrente',
'edit profile': 'modifica profilo', 'edit profile': 'modifica profilo',
'Edit This App': 'Modifica questa applicazione',
'edit views:': 'modifica viste (view):', 'edit views:': 'modifica viste (view):',
'Editing file "%s"': 'Modifica del file "%s"',
'Editing Language file': 'Modifica file linguaggio',
'Enterprise Web Framework': 'Enterprise Web Framework',
'Error logs for "%(app)s"': 'Log degli errori per "%(app)s"',
'Error snapshot': 'Error snapshot',
'Error ticket': 'Error ticket',
'Errors': 'errori',
'Exception instance attributes': 'Exception instance attributes',
'Expand Abbreviation': 'Expand Abbreviation',
'export as csv file': 'esporta come file CSV', 'export as csv file': 'esporta come file CSV',
'exposes': 'espone', 'exposes': 'espone',
'exposes:': 'exposes:',
'extends': 'estende', 'extends': 'estende',
'failed to reload module because:': 'ricaricamento modulo fallito perché:', 'failed to reload module because:': 'ricaricamento modulo fallito perché:',
'file "%(filename)s" created': 'creato il file "%(filename)s"', 'file "%(filename)s" created': 'creato il file "%(filename)s"',
@@ -195,73 +138,188 @@
'file does not exist': 'file inesistente', 'file does not exist': 'file inesistente',
'file saved on %(time)s': "file salvato nell'istante %(time)s", 'file saved on %(time)s': "file salvato nell'istante %(time)s",
'file saved on %s': 'file salvato: %s', 'file saved on %s': 'file salvato: %s',
'filter': 'filter',
'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'I test delle funzioni senza "doctests" risulteranno sempre [passed].',
'Get from URL:': 'Get from URL:',
'Git Pull': 'Git Pull',
'Git Push': 'Git Push',
'Hello World': 'Salve Mondo',
'Help': 'aiuto',
'htmledit': 'modifica come html', 'htmledit': 'modifica come html',
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.',
'Import/Export': 'Importa/Esporta',
'includes': 'include', 'includes': 'include',
'Index': 'Indice',
'insert new': 'inserisci nuovo', 'insert new': 'inserisci nuovo',
'insert new %s': 'inserisci nuovo %s', 'insert new %s': 'inserisci nuovo %s',
'inspect attributes': 'inspect attributes',
'Install': 'installa',
'Installed applications': 'Applicazioni installate',
'internal error': 'errore interno', 'internal error': 'errore interno',
'Internal State': 'Stato interno',
'Invalid action': 'Azione non valida',
'invalid password': 'password non valida', 'invalid password': 'password non valida',
'Invalid Query': 'Richiesta (query) non valida',
'invalid request': 'richiesta non valida', 'invalid request': 'richiesta non valida',
'invalid ticket': 'ticket non valido', 'invalid ticket': 'ticket non valido',
'Key bindings': 'Key bindings',
'Key bindings for ZenCoding Plugin': 'Key bindings for ZenCoding Plugin',
'language file "%(filename)s" created/updated': 'file linguaggio "%(filename)s" creato/aggiornato', 'language file "%(filename)s" created/updated': 'file linguaggio "%(filename)s" creato/aggiornato',
'Language files (static strings) updated': 'Linguaggi (documenti con stringhe statiche) aggiornati',
'languages': 'linguaggi', 'languages': 'linguaggi',
'Languages': 'Linguaggi',
'Last saved on:': 'Ultimo salvataggio:',
'Layout': 'Layout',
'License for': 'Licenza relativa a',
'loading...': 'caricamento...', 'loading...': 'caricamento...',
'locals': 'locals',
'login': 'accesso', 'login': 'accesso',
'Login': 'Accesso',
'Login to the Administrative Interface': "Accesso all'interfaccia amministrativa",
'Logout': 'uscita',
'Main Menu': 'Menu principale',
'Menu Model': 'Menu Modelli',
'merge': 'unisci', 'merge': 'unisci',
'models': 'modelli', 'models': 'modelli',
'Models': 'Modelli',
'Modules': 'Moduli',
'modules': 'moduli', 'modules': 'moduli',
'new application "%s" created': 'creata la nuova applicazione "%s"', 'new application "%s" created': 'creata la nuova applicazione "%s"',
'New application wizard': 'New application wizard',
'new plugin installed': 'installato nuovo plugin', 'new plugin installed': 'installato nuovo plugin',
'New Record': 'Nuovo elemento (record)',
'new record inserted': 'nuovo record inserito', 'new record inserted': 'nuovo record inserito',
'New simple application': 'New simple application',
'next 100 rows': 'prossime 100 righe', 'next 100 rows': 'prossime 100 righe',
'NO': 'NO',
'No databases in this application': 'Nessun database presente in questa applicazione',
'no match': 'nessuna corrispondenza', 'no match': 'nessuna corrispondenza',
'or import from csv file': 'oppure importa da file CSV', 'or import from csv file': 'oppure importa da file CSV',
'or provide app url:': "oppure fornisci url dell'applicazione:", 'or provide app url:': "oppure fornisci url dell'applicazione:",
'Original/Translation': 'Originale/Traduzione',
'Overwrite installed app': 'sovrascrivi applicazione installata',
'Pack all': 'crea pacchetto',
'Pack compiled': 'crea pacchetto del codice compilato',
'pack plugin': 'crea pacchetto del plugin', 'pack plugin': 'crea pacchetto del plugin',
'PAM authenticated user, cannot change password here': 'utente autenticato tramite PAM, impossibile modificare password qui',
'password changed': 'password modificata', 'password changed': 'password modificata',
'Peeking at file': 'Uno sguardo al file',
'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" cancellato', 'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" cancellato',
'Plugin "%s" in application': 'Plugin "%s" nell\'applicazione',
'plugins': 'plugins',
'Plugins': 'I Plugins',
'Plural-Forms:': 'Plural-Forms:',
'Powered by': 'Powered by',
'previous 100 rows': '100 righe precedenti', 'previous 100 rows': '100 righe precedenti',
'private files': 'private files',
'Private files': 'Private files',
'Query:': 'Richiesta (query):',
'record': 'record', 'record': 'record',
'record does not exist': 'il record non esiste', 'record does not exist': 'il record non esiste',
'record id': 'ID del record', 'record id': 'ID del record',
'register': 'registrazione', 'register': 'registrazione',
'Remove compiled': 'rimozione codice compilato',
'request': 'request',
'Resolve Conflict file': 'File di risoluzione conflitto',
'response': 'response',
'restore': 'ripristino', 'restore': 'ripristino',
'revert': 'versione precedente', 'revert': 'versione precedente',
'Rows in table': 'Righe nella tabella',
'Rows selected': 'Righe selezionate',
'rules are not defined': 'rules are not defined',
"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')",
'Running on %s': 'Running on %s',
'Save': 'Save',
'Save via Ajax': 'Save via Ajax',
'Saved file hash:': 'Hash del file salvato:',
'selected': 'selezionato', 'selected': 'selezionato',
'session': 'session',
'session expired': 'sessions scaduta', 'session expired': 'sessions scaduta',
'shell': 'shell', 'shell': 'shell',
'Site': 'sito',
'some files could not be removed': 'non è stato possibile rimuovere alcuni files', 'some files could not be removed': 'non è stato possibile rimuovere alcuni files',
'Start wizard': 'start wizard',
'state': 'stato', 'state': 'stato',
'static': 'statico', 'static': 'statico',
'Static files': 'Files statici',
'Stylesheet': 'Foglio di stile (stylesheet)',
'submit': 'invia', 'submit': 'invia',
'Submit': 'Submit',
'Sure you want to delete this object?': 'Vuoi veramente cancellare questo oggetto?',
'table': 'tabella', 'table': 'tabella',
'test': 'test', 'test': 'test',
'Testing application': 'Test applicazione in corsg',
'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La richiesta (query) è una condizione come ad esempio "db.tabella1.campo1==\'valore\'". Una condizione come "db.tabella1.campo1==db.tabella2.campo2" produce un "JOIN" SQL.',
'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller',
'the application logic, each URL path is mapped in one exposed function in the controller': 'logica dell\'applicazione, ogni percorso "URL" corrisponde ad una funzione esposta da un controller', 'the application logic, each URL path is mapped in one exposed function in the controller': 'logica dell\'applicazione, ogni percorso "URL" corrisponde ad una funzione esposta da un controller',
'The data representation, define database tables and sets': 'The data representation, define database tables and sets',
'the data representation, define database tables and sets': 'rappresentazione dei dati, definizione di tabelle di database e di "set" ', 'the data representation, define database tables and sets': 'rappresentazione dei dati, definizione di tabelle di database e di "set" ',
'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates',
'the presentations layer, views are also known as templates': 'Presentazione dell\'applicazione, viste (views, chiamate anche "templates")', 'the presentations layer, views are also known as templates': 'Presentazione dell\'applicazione, viste (views, chiamate anche "templates")',
'There are no controllers': 'Non ci sono controller',
'There are no models': 'Non ci sono modelli',
'There are no modules': 'Non ci sono moduli',
'There are no plugins': 'There are no plugins',
'There are no static files': 'Non ci sono file statici',
'There are no translators, only default language is supported': 'Non ci sono traduzioni, viene solo supportato il linguaggio di base',
'There are no views': 'Non ci sono viste ("view")',
'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app',
'these files are served without processing, your images go here': 'questi files vengono serviti così come sono, le immagini vanno qui', 'these files are served without processing, your images go here': 'questi files vengono serviti così come sono, le immagini vanno qui',
'These files are served without processing, your images go here': 'These files are served without processing, your images go here',
'This is the %(filename)s template': 'Questo è il template %(filename)s',
'Ticket': 'Ticket',
'Ticket ID': 'Ticket ID',
'TM': 'TM',
'to previous version.': 'torna a versione precedente', 'to previous version.': 'torna a versione precedente',
'To create a plugin, name a file/folder plugin_[name]': 'Per creare un plugin, chiamare un file o cartella plugin_[nome]',
'toggle breakpoint': 'toggle breakpoint',
'Toggle Fullscreen': 'Toggle Fullscreen',
'Traceback': 'Traceback',
'translation strings for the application': "stringhe di traduzioni per l'applicazione", 'translation strings for the application': "stringhe di traduzioni per l'applicazione",
'Translation strings for the application': 'Translation strings for the application',
'try': 'prova', 'try': 'prova',
'try something like': 'prova qualcosa come', 'try something like': 'prova qualcosa come',
'try view': 'try view',
'Unable to check for upgrades': 'Impossibile controllare presenza di aggiornamenti',
'unable to create application "%s"': 'impossibile creare applicazione "%s"', 'unable to create application "%s"': 'impossibile creare applicazione "%s"',
'unable to delete file "%(filename)s"': 'impossibile rimuovere file "%(plugin)s"', 'unable to delete file "%(filename)s"': 'impossibile rimuovere file "%(plugin)s"',
'unable to delete file plugin "%(plugin)s"': 'impossibile rimuovere file di plugin "%(plugin)s"', 'unable to delete file plugin "%(plugin)s"': 'impossibile rimuovere file di plugin "%(plugin)s"',
'Unable to download app because:': 'Impossibile scaricare applicazione perché',
'Unable to download because': 'Impossibile scaricare perché',
'Unable to download because:': 'Unable to download because:',
'unable to parse csv file': 'non riesco a decodificare questo file CSV', 'unable to parse csv file': 'non riesco a decodificare questo file CSV',
'unable to uninstall "%s"': 'impossibile disinstallare "%s"', 'unable to uninstall "%s"': 'impossibile disinstallare "%s"',
'unable to upgrade because "%s"': 'impossibile aggiornare perché "%s"', 'unable to upgrade because "%s"': 'impossibile aggiornare perché "%s"',
'uncheck all': 'smarca tutti', 'uncheck all': 'smarca tutti',
'Uninstall': 'disinstalla',
'update': 'aggiorna', 'update': 'aggiorna',
'update all languages': 'aggiorna tutti i linguaggi', 'update all languages': 'aggiorna tutti i linguaggi',
'Update:': 'Aggiorna:',
'upgrade web2py now': 'upgrade web2py now', 'upgrade web2py now': 'upgrade web2py now',
'upload': 'upload',
'Upload & install packed application': 'Carica ed installa pacchetto con applicazione',
'Upload a package:': 'Upload a package:',
'Upload and install packed application': 'Upload and install packed application',
'upload application:': 'carica applicazione:', 'upload application:': 'carica applicazione:',
'upload file:': 'carica file:', 'upload file:': 'carica file:',
'upload plugin file:': 'carica file di plugin:', 'upload plugin file:': 'carica file di plugin:',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Per costruire richieste (query) più complesse si usano (...)&(...) come "e" (AND), (...)|(...) come "o" (OR), e ~(...) come negazione (NOT).',
'Use an url:': 'Use an url:',
'variables': 'variables', 'variables': 'variables',
'Version': 'Versione',
'Version %s.%s.%s (%s) %s': 'Version %s.%s.%s (%s) %s',
'versioning': 'sistema di versioni', 'versioning': 'sistema di versioni',
'Versioning': 'Versioning',
'view': 'vista', 'view': 'vista',
'View': 'Vista',
'Views': 'viste',
'views': 'viste', 'views': 'viste',
'web2py Recent Tweets': 'Tweets recenti per web2py', 'Web Framework': 'Web Framework',
'web2py is up to date': 'web2py è aggiornato', 'web2py is up to date': 'web2py è aggiornato',
'web2py Recent Tweets': 'Tweets recenti per web2py',
'web2py upgraded; please restart it': 'web2py aggiornato; prego riavviarlo', 'web2py upgraded; please restart it': 'web2py aggiornato; prego riavviarlo',
'Welcome %s': 'Benvenuto %s',
'Welcome to web2py': 'Benvenuto su web2py',
'YES': 'SI',
} }
+17 -3
View File
@@ -123,6 +123,21 @@ if session.authorized:
else: else:
session.last_time = t0 session.last_time = t0
if request.vars.is_mobile in ('true','false','auto'):
session.is_mobile = request.vars.is_mobile or 'auto'
if request.controller=='default' and request.function=='index':
if not request.vars.is_mobile:
session.is_mobile = 'auto'
if not session.is_mobile:
session.is_mobile = 'auto'
if session.is_mobile == 'true':
is_mobile = True
elif session.is_mobile == 'false':
is_mobile = False
else:
is_mobile = request.user_agent().is_mobile
if request.controller == "webservices": if request.controller == "webservices":
basic = request.env.http_authorization basic = request.env.http_authorization
if not basic or not basic[:6].lower() == 'basic ': if not basic or not basic[:6].lower() == 'basic ':
@@ -132,8 +147,8 @@ if request.controller == "webservices":
time.sleep(10) time.sleep(10)
raise HTTP(403,"Not authorized") raise HTTP(403,"Not authorized")
elif not session.authorized and not \ elif not session.authorized and not \
(request.controller == 'default' and \ (request.controller+'/'+request.function in
request.function in ('index','user')): ('default/index','default/user','plugin_jqmobile/index','plugin_jqmobile/about')):
if request.env.query_string: if request.env.query_string:
query_string = '?' + request.env.query_string query_string = '?' + request.env.query_string
@@ -150,7 +165,6 @@ elif session.authorized and \
request.function == 'index': request.function == 'index':
redirect(URL(request.application, 'default', 'site')) redirect(URL(request.application, 'default', 'site'))
if request.controller=='appadmin' and DEMO_MODE: if request.controller=='appadmin' and DEMO_MODE:
session.flash = 'Appadmin disabled in demo mode' session.flash = 'Appadmin disabled in demo mode'
redirect(URL('default','sites')) redirect(URL('default','sites'))
+1 -1
View File
@@ -8,7 +8,7 @@ def A_button(*a,**b):
return A(*a,**b) return A(*a,**b)
def button(href, label): def button(href, label):
if request.user_agent().is_mobile: if is_mobile:
ret = A_button(SPAN(label), _href=href) ret = A_button(SPAN(label), _href=href)
else: else:
ret = A(SPAN(label),_class='button',_href=href) ret = A(SPAN(label),_class='button',_href=href)
File diff suppressed because one or more lines are too long
+10 -8
View File
@@ -10,7 +10,7 @@ h3 {font-size:2.00em}
h4 {font-size:1.50em} h4 {font-size:1.50em}
h5 {font-size:1.25em} h5 {font-size:1.25em}
h6 {font-size:1.12em} h6 {font-size:1.12em}
th,label {font-weight:bold; white-space:nowrap} th,label {font-weight:bold; white-space:nowrap;}
td,th {text-align:left; padding:2px 5px 2px 5px} td,th {text-align:left; padding:2px 5px 2px 5px}
th {vertical-align:middle; border-right:1px solid white} th {vertical-align:middle; border-right:1px solid white}
td {vertical-align:top} td {vertical-align:top}
@@ -69,7 +69,6 @@ fieldset {padding:16px; border-top:1px #DEDEDE solid}
fieldset legend {text-transform:uppercase; font-weight:bold; padding:4px 16px 4px 16px; background:#f1f1f1} fieldset legend {text-transform:uppercase; font-weight:bold; padding:4px 16px 4px 16px; background:#f1f1f1}
/* fix ie problem with menu */ /* fix ie problem with menu */
.ie-lte7 .topbar .container {z-index:2}
td.w2p_fw {padding-bottom:1px} td.w2p_fw {padding-bottom:1px}
td.w2p_fl,td.w2p_fw,td.w2p_fc {vertical-align:top} td.w2p_fl,td.w2p_fw,td.w2p_fc {vertical-align:top}
@@ -187,10 +186,7 @@ div.error {
.web2py_paginator {} .web2py_paginator {}
.web2py_grid {width:100%} .web2py_grid {width:100%}
.web2py_grid table {width:100%} .web2py_grid table {width:100%}
.web2py_grid tbody td { .web2py_grid tbody td {padding:2px 5px 2px 5px; vertical-align: middle;}
padding:2px 5px 2px 5px;
vertical-align:middle;
}
.web2py_grid thead th,.web2py_grid tfoot td { .web2py_grid thead th,.web2py_grid tfoot td {
background-color:#EAEAEA; background-color:#EAEAEA;
@@ -299,10 +295,16 @@ li.w2p_grid_breadcrumb_elem {
.web2py_console input, .web2py_console select, .web2py_console input, .web2py_console select,
.web2py_console a { margin: 2px; } .web2py_console a { margin: 2px; }
.ie9 #w2p_query_panel {padding-bottom:2px}
#wiki_page_body { #wiki_page_body {
width: 600px; width: 600px;
height: auto; height: auto;
min-height: 400px; min-height: 400px;
} }
/* fix some IE problems */
.ie-lte7 .topbar .container {z-index:2}
.ie-lte8 div.flash{ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#222222', endColorstr='#000000', GradientType=0 ); }
.ie-lte8 div.flash:hover {filter:alpha(opacity=25);}
.ie9 #w2p_query_panel {padding-bottom:2px}
File diff suppressed because one or more lines are too long
+49 -14
View File
@@ -39,7 +39,7 @@ function web2py_ajax_init(target) {
function web2py_event_handlers() { function web2py_event_handlers() {
var doc = jQuery(document) var doc = jQuery(document)
doc.on('click', '.flash', function(e){jQuery(this).fadeOut('slow'); e.preventDefault();}); doc.on('click', '.flash', function(e){var t=jQuery(this); if(t.css('top')=='0px') t.slideUp('slow'); else t.fadeOut(); e.preventDefault();});
doc.on('keyup', 'input.integer', function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); doc.on('keyup', 'input.integer', function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();});
doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();}); doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();});
var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?"; var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?";
@@ -55,7 +55,7 @@ function web2py_event_handlers() {
jQuery(function() { jQuery(function() {
var flash = jQuery('.flash'); var flash = jQuery('.flash');
flash.hide(); flash.hide();
if(flash.html()) flash.slideDown(); if(flash.html()) flash.append('<span style="float:right;">&times;</span>').slideDown();
web2py_ajax_init(document); web2py_ajax_init(document);
web2py_event_handlers(); web2py_event_handlers();
}); });
@@ -67,20 +67,20 @@ function web2py_trap_form(action,target) {
form.submit(function(e){ form.submit(function(e){
jQuery('.flash').hide().html(''); jQuery('.flash').hide().html('');
web2py_ajax_page('post',action,form.serialize(),target); web2py_ajax_page('post',action,form.serialize(),target);
e.preventDefault(); e.preventDefault();
}); });
}); });
} }
function web2py_trap_link(target) { function web2py_trap_link(target) {
jQuery('#'+target+' a.w2p_trap').each(function(i){ jQuery('#'+target+' a.w2p_trap').each(function(i){
var link=jQuery(this); var link=jQuery(this);
link.click(function(e) { link.click(function(e) {
jQuery('.flash').hide().html(''); jQuery('.flash').hide().html('');
web2py_ajax_page('get',link.attr('href'),[],target); web2py_ajax_page('get',link.attr('href'),[],target);
e.preventDefault(); e.preventDefault();
}); });
}); });
} }
function web2py_ajax_page(method, action, data, target) { function web2py_ajax_page(method, action, data, target) {
@@ -101,9 +101,9 @@ function web2py_ajax_page(method, action, data, target) {
web2py_trap_link(target); web2py_trap_link(target);
web2py_ajax_init('#'+target); web2py_ajax_init('#'+target);
if(command) if(command)
eval(decodeURIComponent(command)); eval(decodeURIComponent(command));
if(flash) if(flash)
jQuery('.flash').html(decodeURIComponent(flash)).slideDown(); jQuery('.flash').html(decodeURIComponent(flash)).slideDown();
} }
}); });
} }
@@ -151,11 +151,11 @@ function web2py_component(action, target, timeout, times){
} }
} else { } else {
// run once (no timeout specified) // run once (no timeout specified)
element.reload_counter = Infinity; element.reload_counter = Infinity;
web2py_ajax_page('get', action, null, target); web2py_ajax_page('get', action, null, target);
} }); } } }); }
function web2py_comet(url,onmessage,onopen,onclose) { function web2py_websocket(url,onmessage,onopen,onclose) {
if ("WebSocket" in window) { if ("WebSocket" in window) {
var ws = new WebSocket(url); var ws = new WebSocket(url);
ws.onopen = onopen?onopen:(function(){}); ws.onopen = onopen?onopen:(function(){});
@@ -165,3 +165,38 @@ function web2py_comet(url,onmessage,onopen,onclose) {
} else return false; // not supported } else return false; // not supported
} }
function web2py_calc_entropy(mystring) {
//calculate a simple entropy for a given string
var csets = new Array(
'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'0123456789', '!@#$\%^&*()', '~`-_=+[]{}\|;:\'",.<>?/',
'0123456789abcdefghijklmnopqrstuvwxyz');
var score = 0, other = {}, seen = {}, lastset = null, mystringlist = mystring.split('');
for (var i=0;i<mystringlist.length;i++) { // classify this character
var c = mystringlist[i], inset=5;
for(var j = 0; j<csets.length; j++)
if (csets[j].indexOf(c) != -1) {inset = j; break;}
//calculate effect of character on alphabet size
if(!(inset in seen)) {seen[inset] = 1;score += csets[inset].length;}
else if (!(c in other)) {score += 1;other[c] = 1;}
if (inset != lastset) {score += 1;lastset = inset;}
}
var entropy = mystring.length*Math.log(score)/0.6931471805599453;
return Math.round(entropy*100)/100
}
function web2py_validate_entropy(myfield, req_entropy) {
var validator = function () {
var v = (web2py_calc_entropy(myfield.val())||0)/req_entropy;
var r=0,g=0,b=0,rs=function(x){return Math.round(x*15).toString(16)};
if(v<=0.5) {r=1.0; g=2.0*v;}
else {r=(1.0-2.0*(Math.max(v,0)-0.5)); g=1.0;}
var color = '#'+rs(r)+rs(g)+rs(b);
myfield.css('background-color',color);
entropy_callback = myfield.data('entropy_callback');
if(entropy_callback) entropy_callback(v);
}
if(!myfield.hasClass('entropy_check')) myfield.on('keyup', validator).on('keydown', validator).addClass('entropy_check');
}
File diff suppressed because one or more lines are too long
@@ -1,173 +0,0 @@
/*! jQuery Mobile v1.0 jquerymobile.com | jquery.org/license */
(function(a,e){if(a.cleanData){var b=a.cleanData;a.cleanData=function(f){for(var c=0,h;(h=f[c])!=null;c++)a(h).triggerHandler("remove");b(f)}}else{var d=a.fn.remove;a.fn.remove=function(b,c){return this.each(function(){c||(!b||a.filter(b,[this]).length)&&a("*",this).add([this]).each(function(){a(this).triggerHandler("remove")});return d.call(a(this),b,c)})}}a.widget=function(b,c,h){var d=b.split(".")[0],e,b=b.split(".")[1];e=d+"-"+b;if(!h)h=c,c=a.Widget;a.expr[":"][e]=function(c){return!!a.data(c,
b)};a[d]=a[d]||{};a[d][b]=function(a,b){arguments.length&&this._createWidget(a,b)};c=new c;c.options=a.extend(true,{},c.options);a[d][b].prototype=a.extend(true,c,{namespace:d,widgetName:b,widgetEventPrefix:a[d][b].prototype.widgetEventPrefix||b,widgetBaseClass:e},h);a.widget.bridge(b,a[d][b])};a.widget.bridge=function(b,c){a.fn[b]=function(d){var g=typeof d==="string",i=Array.prototype.slice.call(arguments,1),k=this,d=!g&&i.length?a.extend.apply(null,[true,d].concat(i)):d;if(g&&d.charAt(0)==="_")return k;
g?this.each(function(){var c=a.data(this,b);if(!c)throw"cannot call methods on "+b+" prior to initialization; attempted to call method '"+d+"'";if(!a.isFunction(c[d]))throw"no such method '"+d+"' for "+b+" widget instance";var g=c[d].apply(c,i);if(g!==c&&g!==e)return k=g,false}):this.each(function(){var e=a.data(this,b);e?e.option(d||{})._init():a.data(this,b,new c(d,this))});return k}};a.Widget=function(a,b){arguments.length&&this._createWidget(a,b)};a.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",
options:{disabled:false},_createWidget:function(b,c){a.data(c,this.widgetName,this);this.element=a(c);this.options=a.extend(true,{},this.options,this._getCreateOptions(),b);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()});this._create();this._trigger("create");this._init()},_getCreateOptions:function(){var b={};a.metadata&&(b=a.metadata.get(element)[this.widgetName]);return b},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);
this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled ui-state-disabled")},widget:function(){return this.element},option:function(b,c){var d=b;if(arguments.length===0)return a.extend({},this.options);if(typeof b==="string"){if(c===e)return this.options[b];d={};d[b]=c}this._setOptions(d);return this},_setOptions:function(b){var c=this;a.each(b,function(a,b){c._setOption(a,b)});return this},_setOption:function(a,b){this.options[a]=b;a==="disabled"&&
this.widget()[b?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",b);return this},enable:function(){return this._setOption("disabled",false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(b,c,d){var e=this.options[b],c=a.Event(c);c.type=(b===this.widgetEventPrefix?b:this.widgetEventPrefix+b).toLowerCase();d=d||{};if(c.originalEvent)for(var b=a.event.props.length,i;b;)i=a.event.props[--b],c[i]=c.originalEvent[i];this.element.trigger(c,
d);return!(a.isFunction(e)&&e.call(this.element[0],c,d)===false||c.isDefaultPrevented())}}})(jQuery);
(function(a,e){a.widget("mobile.widget",{_createWidget:function(){a.Widget.prototype._createWidget.apply(this,arguments);this._trigger("init")},_getCreateOptions:function(){var b=this.element,d={};a.each(this.options,function(a){var c=b.jqmData(a.replace(/[A-Z]/g,function(a){return"-"+a.toLowerCase()}));c!==e&&(d[a]=c)});return d},enhanceWithin:function(b){var d=a(b).closest(":jqmData(role='page')").data("page"),d=d&&d.keepNativeSelector()||"";a(this.options.initSelector,b).not(d)[this.widgetName]()}})})(jQuery);
(function(a){a(window);var e=a("html");a.mobile.media=function(){var b={},d=a("<div id='jquery-mediatest'>"),f=a("<body>").append(d);return function(a){if(!(a in b)){var h=document.createElement("style"),g="@media "+a+" { #jquery-mediatest { position:absolute; } }";h.type="text/css";h.styleSheet?h.styleSheet.cssText=g:h.appendChild(document.createTextNode(g));e.prepend(f).prepend(h);b[a]=d.css("position")==="absolute";f.add(h).remove()}return b[a]}}()})(jQuery);
(function(a,e){function b(a){var b=a.charAt(0).toUpperCase()+a.substr(1),a=(a+" "+c.join(b+" ")+b).split(" "),d;for(d in a)if(f[a[d]]!==e)return true}var d=a("<body>").prependTo("html"),f=d[0].style,c=["Webkit","Moz","O"],h="palmGetResource"in window,g=window.operamini&&{}.toString.call(window.operamini)==="[object OperaMini]",i=window.blackberry;a.mobile.browser={};a.mobile.browser.ie=function(){for(var a=3,b=document.createElement("div"),c=b.all||[];b.innerHTML="<\!--[if gt IE "+ ++a+"]><br><![endif]--\>",
c[0];);return a>4?a:!a}();a.extend(a.support,{orientation:"orientation"in window&&"onorientationchange"in window,touch:"ontouchend"in document,cssTransitions:"WebKitTransitionEvent"in window,pushState:"pushState"in history&&"replaceState"in history,mediaquery:a.mobile.media("only all"),cssPseudoElement:!!b("content"),touchOverflow:!!b("overflowScrolling"),boxShadow:!!b("boxShadow")&&!i,scrollTop:("pageXOffset"in window||"scrollTop"in document.documentElement||"scrollTop"in d[0])&&!h&&!g,dynamicBaseTag:function(){var b=
location.protocol+"//"+location.host+location.pathname+"ui-dir/",c=a("head base"),f=null,e="",h;c.length?e=c.attr("href"):c=f=a("<base>",{href:b}).appendTo("head");h=a("<a href='testurl' />").prependTo(d)[0].href;c[0].href=e||location.pathname;f&&f.remove();return h.indexOf(b)===0}()});d.remove();h=function(){var a=window.navigator.userAgent;return a.indexOf("Nokia")>-1&&(a.indexOf("Symbian/3")>-1||a.indexOf("Series60/5")>-1)&&a.indexOf("AppleWebKit")>-1&&a.match(/(BrowserNG|NokiaBrowser)\/7\.[0-3]/)}();
a.mobile.ajaxBlacklist=window.blackberry&&!window.WebKitPoint||g||h;h&&a(function(){a("head link[rel='stylesheet']").attr("rel","alternate stylesheet").attr("rel","stylesheet")});a.support.boxShadow||a("html").addClass("ui-mobile-nosupport-boxshadow")})(jQuery);
(function(a,e,b,d){function f(a){for(;a&&typeof a.originalEvent!=="undefined";)a=a.originalEvent;return a}function c(b){for(var c={},f,d;b;){f=a.data(b,n);for(d in f)if(f[d])c[d]=c.hasVirtualBinding=true;b=b.parentNode}return c}function h(){v&&(clearTimeout(v),v=0);v=setTimeout(function(){E=v=0;u.length=0;D=false;y=true},a.vmouse.resetTimerDuration)}function g(b,c,r){var e,h;if(!(h=r&&r[b])){if(r=!r)a:{for(r=c.target;r;){if((h=a.data(r,n))&&(!b||h[b]))break a;r=r.parentNode}r=null}h=r}if(h){e=c;var r=
e.type,j,g;e=a.Event(e);e.type=b;h=e.originalEvent;j=a.event.props;if(h)for(g=j.length;g;)b=j[--g],e[b]=h[b];if(r.search(/mouse(down|up)|click/)>-1&&!e.which)e.which=1;if(r.search(/^touch/)!==-1&&(b=f(h),r=b.touches,b=b.changedTouches,r=r&&r.length?r[0]:b&&b.length?b[0]:d))for(h=0,len=z.length;h<len;h++)b=z[h],e[b]=r[b];a(c.target).trigger(e)}return e}function i(b){var c=a.data(b.target,A);if(!D&&(!E||E!==c))if(c=g("v"+b.type,b))c.isDefaultPrevented()&&b.preventDefault(),c.isPropagationStopped()&&
b.stopPropagation(),c.isImmediatePropagationStopped()&&b.stopImmediatePropagation()}function k(b){var d=f(b).touches,e;if(d&&d.length===1&&(e=b.target,d=c(e),d.hasVirtualBinding))E=r++,a.data(e,A,E),v&&(clearTimeout(v),v=0),w=y=false,e=f(b).touches[0],x=e.pageX,t=e.pageY,g("vmouseover",b,d),g("vmousedown",b,d)}function l(a){y||(w||g("vmousecancel",a,c(a.target)),w=true,h())}function o(b){if(!y){var d=f(b).touches[0],r=w,e=a.vmouse.moveDistanceThreshold;w=w||Math.abs(d.pageX-x)>e||Math.abs(d.pageY-
t)>e;flags=c(b.target);w&&!r&&g("vmousecancel",b,flags);g("vmousemove",b,flags);h()}}function m(a){if(!y){y=true;var b=c(a.target),d;g("vmouseup",a,b);if(!w&&(d=g("vclick",a,b))&&d.isDefaultPrevented())d=f(a).changedTouches[0],u.push({touchID:E,x:d.clientX,y:d.clientY}),D=true;g("vmouseout",a,b);w=false;h()}}function p(b){var b=a.data(b,n),c;if(b)for(c in b)if(b[c])return true;return false}function j(){}function q(b){var c=b.substr(1);return{setup:function(){p(this)||a.data(this,n,{});a.data(this,
n)[b]=true;s[b]=(s[b]||0)+1;s[b]===1&&B.bind(c,i);a(this).bind(c,j);if(C)s.touchstart=(s.touchstart||0)+1,s.touchstart===1&&B.bind("touchstart",k).bind("touchend",m).bind("touchmove",o).bind("scroll",l)},teardown:function(){--s[b];s[b]||B.unbind(c,i);C&&(--s.touchstart,s.touchstart||B.unbind("touchstart",k).unbind("touchmove",o).unbind("touchend",m).unbind("scroll",l));var d=a(this),f=a.data(this,n);f&&(f[b]=false);d.unbind(c,j);p(this)||d.removeData(n)}}}var n="virtualMouseBindings",A="virtualTouchID",
e="vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split(" "),z="clientX clientY pageX pageY screenX screenY".split(" "),s={},v=0,x=0,t=0,w=false,u=[],D=false,y=false,C="addEventListener"in b,B=a(b),r=1,E=0;a.vmouse={moveDistanceThreshold:10,clickDistanceThreshold:10,resetTimerDuration:1500};for(var F=0;F<e.length;F++)a.event.special[e[F]]=q(e[F]);C&&b.addEventListener("click",function(b){var c=u.length,d=b.target,f,r,e,h,j;if(c){f=b.clientX;r=b.clientY;threshold=a.vmouse.clickDistanceThreshold;
for(e=d;e;){for(h=0;h<c;h++)if(j=u[h],e===d&&Math.abs(j.x-f)<threshold&&Math.abs(j.y-r)<threshold||a.data(e,A)===j.touchID){b.preventDefault();b.stopPropagation();return}e=e.parentNode}}},true)})(jQuery,window,document);
(function(a,e,b){function d(b,c,d){var f=d.type;d.type=c;a.event.handle.call(b,d);d.type=f}a.each("touchstart touchmove touchend orientationchange throttledresize tap taphold swipe swipeleft swiperight scrollstart scrollstop".split(" "),function(b,c){a.fn[c]=function(a){return a?this.bind(c,a):this.trigger(c)};a.attrFn[c]=true});var f=a.support.touch,c=f?"touchstart":"mousedown",h=f?"touchend":"mouseup",g=f?"touchmove":"mousemove";a.event.special.scrollstart={enabled:true,setup:function(){function b(a,
e){f=e;d(c,f?"scrollstart":"scrollstop",a)}var c=this,f,e;a(c).bind("touchmove scroll",function(c){a.event.special.scrollstart.enabled&&(f||b(c,true),clearTimeout(e),e=setTimeout(function(){b(c,false)},50))})}};a.event.special.tap={setup:function(){var b=this,c=a(b);c.bind("vmousedown",function(f){function e(){clearTimeout(q)}function h(){e();c.unbind("vclick",g).unbind("vmouseup",e).unbind("vmousecancel",h)}function g(a){h();j==a.target&&d(b,"tap",a)}if(f.which&&f.which!==1)return false;var j=f.target,
q;c.bind("vmousecancel",h).bind("vmouseup",e).bind("vclick",g);q=setTimeout(function(){d(b,"taphold",a.Event("taphold"))},750)})}};a.event.special.swipe={scrollSupressionThreshold:10,durationThreshold:1E3,horizontalDistanceThreshold:30,verticalDistanceThreshold:75,setup:function(){var d=a(this);d.bind(c,function(c){function f(b){if(m){var c=b.originalEvent.touches?b.originalEvent.touches[0]:b;p={time:(new Date).getTime(),coords:[c.pageX,c.pageY]};Math.abs(m.coords[0]-p.coords[0])>a.event.special.swipe.scrollSupressionThreshold&&
b.preventDefault()}}var e=c.originalEvent.touches?c.originalEvent.touches[0]:c,m={time:(new Date).getTime(),coords:[e.pageX,e.pageY],origin:a(c.target)},p;d.bind(g,f).one(h,function(){d.unbind(g,f);m&&p&&p.time-m.time<a.event.special.swipe.durationThreshold&&Math.abs(m.coords[0]-p.coords[0])>a.event.special.swipe.horizontalDistanceThreshold&&Math.abs(m.coords[1]-p.coords[1])<a.event.special.swipe.verticalDistanceThreshold&&m.origin.trigger("swipe").trigger(m.coords[0]>p.coords[0]?"swipeleft":"swiperight");
m=p=b})})}};(function(a,b){function c(){var a=f();a!==e&&(e=a,d.trigger("orientationchange"))}var d=a(b),f,e;a.event.special.orientationchange={setup:function(){if(a.support.orientation&&a.mobile.orientationChangeEnabled)return false;e=f();d.bind("throttledresize",c)},teardown:function(){if(a.support.orientation&&a.mobile.orientationChangeEnabled)return false;d.unbind("throttledresize",c)},add:function(a){var b=a.handler;a.handler=function(a){a.orientation=f();return b.apply(this,arguments)}}};a.event.special.orientationchange.orientation=
f=function(){var c=true,c=document.documentElement;return(c=a.support.orientation?b.orientation%180==0:c&&c.clientWidth/c.clientHeight<1.1)?"portrait":"landscape"}})(jQuery,e);(function(){a.event.special.throttledresize={setup:function(){a(this).bind("resize",b)},teardown:function(){a(this).unbind("resize",b)}};var b=function(){f=(new Date).getTime();e=f-c;e>=250?(c=f,a(this).trigger("throttledresize")):(d&&clearTimeout(d),d=setTimeout(b,250-e))},c=0,d,f,e})();a.each({scrollstop:"scrollstart",taphold:"tap",
swipeleft:"swipe",swiperight:"swipe"},function(b,c){a.event.special[b]={setup:function(){a(this).bind(c,a.noop)}}})})(jQuery,this);
(function(a,e,b){function d(a){a=a||location.href;return"#"+a.replace(/^[^#]*#?(.*)$/,"$1")}var f="hashchange",c=document,h,g=a.event.special,i=c.documentMode,k="on"+f in e&&(i===b||i>7);a.fn[f]=function(a){return a?this.bind(f,a):this.trigger(f)};a.fn[f].delay=50;g[f]=a.extend(g[f],{setup:function(){if(k)return false;a(h.start)},teardown:function(){if(k)return false;a(h.stop)}});h=function(){function h(){var b=d(),c=n(p);if(b!==p)q(p=b,c),a(e).trigger(f);else if(c!==p)location.href=location.href.replace(/#.*/,
"")+c;i=setTimeout(h,a.fn[f].delay)}var g={},i,p=d(),j=function(a){return a},q=j,n=j;g.start=function(){i||h()};g.stop=function(){i&&clearTimeout(i);i=b};a.browser.msie&&!k&&function(){var b,e;g.start=function(){if(!b)e=(e=a.fn[f].src)&&e+d(),b=a('<iframe tabindex="-1" title="empty"/>').hide().one("load",function(){e||q(d());h()}).attr("src",e||"javascript:0").insertAfter("body")[0].contentWindow,c.onpropertychange=function(){try{if(event.propertyName==="title")b.document.title=c.title}catch(a){}}};
g.stop=j;n=function(){return d(b.location.href)};q=function(d,e){var h=b.document,g=a.fn[f].domain;if(d!==e)h.title=c.title,h.open(),g&&h.write('<script>document.domain="'+g+'"<\/script>'),h.close(),b.location.hash=d}}();return g}()})(jQuery,this);
(function(a){a.widget("mobile.page",a.mobile.widget,{options:{theme:"c",domCache:false,keepNativeDefault:":jqmData(role='none'), :jqmData(role='nojs')"},_create:function(){this._trigger("beforecreate");this.element.attr("tabindex","0").addClass("ui-page ui-body-"+this.options.theme)},keepNativeSelector:function(){var e=this.options;return e.keepNative&&a.trim(e.keepNative)&&e.keepNative!==e.keepNativeDefault?[e.keepNative,e.keepNativeDefault].join(", "):e.keepNativeDefault}})})(jQuery);
(function(a,e){var b={};a.extend(a.mobile,{ns:"",subPageUrlKey:"ui-page",activePageClass:"ui-page-active",activeBtnClass:"ui-btn-active",ajaxEnabled:true,hashListeningEnabled:true,linkBindingEnabled:true,defaultPageTransition:"slide",minScrollBack:250,defaultDialogTransition:"pop",loadingMessage:"loading",pageLoadErrorMessage:"Error Loading Page",autoInitializePage:true,pushStateEnabled:true,orientationChangeEnabled:true,gradeA:function(){return a.support.mediaquery||a.mobile.browser.ie&&a.mobile.browser.ie>=
7},keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91},silentScroll:function(b){if(a.type(b)!=="number")b=a.mobile.defaultHomeScroll;a.event.special.scrollstart.enabled=false;
setTimeout(function(){e.scrollTo(0,b);a(document).trigger("silentscroll",{x:0,y:b})},20);setTimeout(function(){a.event.special.scrollstart.enabled=true},150)},nsNormalizeDict:b,nsNormalize:function(c){return!c?void 0:b[c]||(b[c]=a.camelCase(a.mobile.ns+c))},getInheritedTheme:function(a,b){for(var d=a[0],f="",e=/ui-(bar|body)-([a-z])\b/,l,o;d;){l=d.className||"";if((o=e.exec(l))&&(f=o[2]))break;d=d.parentNode}return f||b||"a"}});a.fn.jqmData=function(b,d){var f;typeof b!="undefined"&&(f=this.data(b?
a.mobile.nsNormalize(b):b,d));return f};a.jqmData=function(b,d,f){var e;typeof d!="undefined"&&(e=a.data(b,d?a.mobile.nsNormalize(d):d,f));return e};a.fn.jqmRemoveData=function(b){return this.removeData(a.mobile.nsNormalize(b))};a.jqmRemoveData=function(b,d){return a.removeData(b,a.mobile.nsNormalize(d))};a.fn.removeWithDependents=function(){a.removeWithDependents(this)};a.removeWithDependents=function(b){b=a(b);(b.jqmData("dependents")||a()).remove();b.remove()};a.fn.addDependents=function(b){a.addDependents(a(this),
b)};a.addDependents=function(b,d){var f=a(b).jqmData("dependents")||a();a(b).jqmData("dependents",a.merge(f,d))};a.fn.getEncodedText=function(){return a("<div/>").text(a(this).text()).html()};var d=a.find,f=/:jqmData\(([^)]*)\)/g;a.find=function(b,e,g,i){b=b.replace(f,"[data-"+(a.mobile.ns||"")+"$1]");return d.call(this,b,e,g,i)};a.extend(a.find,d);a.find.matches=function(b,d){return a.find(b,null,null,d)};a.find.matchesSelector=function(b,d){return a.find(d,null,null,[b]).length>0}})(jQuery,this);
(function(a,e){function b(a){var b=a.find(".ui-title:eq(0)");b.length?b.focus():a.focus()}function d(b){q&&(!q.closest(".ui-page-active").length||b)&&q.removeClass(a.mobile.activeBtnClass);q=null}function f(){z=false;A.length>0&&a.mobile.changePage.apply(null,A.pop())}function c(c,d,f,e){var g=a.mobile.urlHistory.getActive(),j=a.support.touchOverflow&&a.mobile.touchOverflowEnabled,i=g.lastScroll||(j?0:a.mobile.defaultHomeScroll),g=h();window.scrollTo(0,a.mobile.defaultHomeScroll);d&&d.data("page")._trigger("beforehide",
null,{nextPage:c});j||c.height(g+i);c.data("page")._trigger("beforeshow",null,{prevPage:d||a("")});a.mobile.hidePageLoadingMsg();j&&i&&(c.addClass("ui-mobile-pre-transition"),b(c),c.is(".ui-native-fixed")?c.find(".ui-content").scrollTop(i):c.scrollTop(i));f=(a.mobile.transitionHandlers[f||"none"]||a.mobile.defaultTransitionHandler)(f,e,c,d);f.done(function(){j||(c.height(""),b(c));j||a.mobile.silentScroll(i);d&&(j||d.height(""),d.data("page")._trigger("hide",null,{nextPage:c}));c.data("page")._trigger("show",
null,{prevPage:d||a("")})});return f}function h(){var b=a.event.special.orientationchange.orientation()==="portrait",c=b?screen.availHeight:screen.availWidth,b=Math.max(b?480:320,a(window).height());return Math.min(c,b)}function g(){(!a.support.touchOverflow||!a.mobile.touchOverflowEnabled)&&a("."+a.mobile.activePageClass).css("min-height",h())}function i(b,c){c&&b.attr("data-"+a.mobile.ns+"role",c);b.page()}function k(a){for(;a;){if(typeof a.nodeName==="string"&&a.nodeName.toLowerCase()=="a")break;
a=a.parentNode}return a}function l(b){var b=a(b).closest(".ui-page").jqmData("url"),c=t.hrefNoHash;if(!b||!j.isPath(b))b=c;return j.makeUrlAbsolute(b,c)}var o=a(window),m=a("html"),p=a("head"),j={urlParseRE:/^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,parseUrl:function(b){if(a.type(b)==="object")return b;b=j.urlParseRE.exec(b||"")||[];return{href:b[0]||"",hrefNoHash:b[1]||
"",hrefNoSearch:b[2]||"",domain:b[3]||"",protocol:b[4]||"",doubleSlash:b[5]||"",authority:b[6]||"",username:b[8]||"",password:b[9]||"",host:b[10]||"",hostname:b[11]||"",port:b[12]||"",pathname:b[13]||"",directory:b[14]||"",filename:b[15]||"",search:b[16]||"",hash:b[17]||""}},makePathAbsolute:function(a,b){if(a&&a.charAt(0)==="/")return a;for(var a=a||"",c=(b=b?b.replace(/^\/|(\/[^\/]*|[^\/]+)$/g,""):"")?b.split("/"):[],d=a.split("/"),f=0;f<d.length;f++){var e=d[f];switch(e){case ".":break;case "..":c.length&&
c.pop();break;default:c.push(e)}}return"/"+c.join("/")},isSameDomain:function(a,b){return j.parseUrl(a).domain===j.parseUrl(b).domain},isRelativeUrl:function(a){return j.parseUrl(a).protocol===""},isAbsoluteUrl:function(a){return j.parseUrl(a).protocol!==""},makeUrlAbsolute:function(a,b){if(!j.isRelativeUrl(a))return a;var c=j.parseUrl(a),d=j.parseUrl(b),f=c.protocol||d.protocol,e=c.protocol?c.doubleSlash:c.doubleSlash||d.doubleSlash,h=c.authority||d.authority,g=c.pathname!=="",i=j.makePathAbsolute(c.pathname||
d.filename,d.pathname);return f+e+h+i+(c.search||!g&&d.search||"")+c.hash},addSearchParams:function(b,c){var d=j.parseUrl(b),f=typeof c==="object"?a.param(c):c,e=d.search||"?";return d.hrefNoSearch+e+(e.charAt(e.length-1)!=="?"?"&":"")+f+(d.hash||"")},convertUrlToDataUrl:function(a){var b=j.parseUrl(a);if(j.isEmbeddedPage(b))return b.hash.split(s)[0].replace(/^#/,"");else if(j.isSameDomain(b,t))return b.hrefNoHash.replace(t.domain,"");return a},get:function(a){if(a===e)a=location.hash;return j.stripHash(a).replace(/[^\/]*\.[^\/*]+$/,
"")},getFilePath:function(b){var c="&"+a.mobile.subPageUrlKey;return b&&b.split(c)[0].split(s)[0]},set:function(a){location.hash=a},isPath:function(a){return/\//.test(a)},clean:function(a){return a.replace(t.domain,"")},stripHash:function(a){return a.replace(/^#/,"")},cleanHash:function(a){return j.stripHash(a.replace(/\?.*$/,"").replace(s,""))},isExternal:function(a){a=j.parseUrl(a);return a.protocol&&a.domain!==x.domain?true:false},hasProtocol:function(a){return/^(:?\w+:)/.test(a)},isFirstPageUrl:function(b){var b=
j.parseUrl(j.makeUrlAbsolute(b,t)),c=a.mobile.firstPage,c=c&&c[0]?c[0].id:e;return(b.hrefNoHash===x.hrefNoHash||w&&b.hrefNoHash===t.hrefNoHash)&&(!b.hash||b.hash==="#"||c&&b.hash.replace(/^#/,"")===c)},isEmbeddedPage:function(a){a=j.parseUrl(a);return a.protocol!==""?a.hash&&(a.hrefNoHash===x.hrefNoHash||w&&a.hrefNoHash===t.hrefNoHash):/^#/.test(a.href)}},q=null,n={stack:[],activeIndex:0,getActive:function(){return n.stack[n.activeIndex]},getPrev:function(){return n.stack[n.activeIndex-1]},getNext:function(){return n.stack[n.activeIndex+
1]},addNew:function(a,b,c,d,f){n.getNext()&&n.clearForward();n.stack.push({url:a,transition:b,title:c,pageUrl:d,role:f});n.activeIndex=n.stack.length-1},clearForward:function(){n.stack=n.stack.slice(0,n.activeIndex+1)},directHashChange:function(b){var c,d,f;this.getActive();a.each(n.stack,function(a,e){b.currentUrl===e.url&&(c=a<n.activeIndex,d=!c,f=a)});this.activeIndex=f!==e?f:this.activeIndex;c?(b.either||b.isBack)(true):d&&(b.either||b.isForward)(false)},ignoreNextHashChange:false},A=[],z=false,
s="&ui-state=dialog",v=p.children("base"),x=j.parseUrl(location.href),t=v.length?j.parseUrl(j.makeUrlAbsolute(v.attr("href"),x.href)):x,w=x.hrefNoHash!==t.hrefNoHash,u=a.support.dynamicBaseTag?{element:v.length?v:a("<base>",{href:t.hrefNoHash}).prependTo(p),set:function(a){u.element.attr("href",j.makeUrlAbsolute(a,t))},reset:function(){u.element.attr("href",t.hrefNoHash)}}:e,D=true,y,C,B;y=function(){var b=o;a.support.touchOverflow&&a.mobile.touchOverflowEnabled&&(b=a(".ui-page-active"),b=b.is(".ui-native-fixed")?
b.find(".ui-content"):b);return b};C=function(b){if(D){var c=a.mobile.urlHistory.getActive();if(c)b=b&&b.scrollTop(),c.lastScroll=b<a.mobile.minScrollBack?a.mobile.defaultHomeScroll:b}};B=function(){setTimeout(C,100,a(this))};o.bind(a.support.pushState?"popstate":"hashchange",function(){D=false});o.one(a.support.pushState?"popstate":"hashchange",function(){D=true});o.one("pagecontainercreate",function(){a.mobile.pageContainer.bind("pagechange",function(){var a=y();D=true;a.unbind("scrollstop",B);
a.bind("scrollstop",B)})});y().bind("scrollstop",B);a.mobile.getScreenHeight=h;a.fn.animationComplete=function(b){return a.support.cssTransitions?a(this).one("webkitAnimationEnd",b):(setTimeout(b,0),a(this))};a.mobile.path=j;a.mobile.base=u;a.mobile.urlHistory=n;a.mobile.dialogHashKey=s;a.mobile.noneTransitionHandler=function(b,c,d,f){f&&f.removeClass(a.mobile.activePageClass);d.addClass(a.mobile.activePageClass);return a.Deferred().resolve(b,c,d,f).promise()};a.mobile.defaultTransitionHandler=a.mobile.noneTransitionHandler;
a.mobile.transitionHandlers={none:a.mobile.defaultTransitionHandler};a.mobile.allowCrossDomainPages=false;a.mobile.getDocumentUrl=function(b){return b?a.extend({},x):x.href};a.mobile.getDocumentBase=function(b){return b?a.extend({},t):t.href};a.mobile._bindPageRemove=function(){var b=a(this);!b.data("page").options.domCache&&b.is(":jqmData(external-page='true')")&&b.bind("pagehide.remove",function(){var b=a(this),c=new a.Event("pageremove");b.trigger(c);c.isDefaultPrevented()||b.removeWithDependents()})};
a.mobile.loadPage=function(b,c){var d=a.Deferred(),f=a.extend({},a.mobile.loadPage.defaults,c),h=null,g=null,m=j.makeUrlAbsolute(b,a.mobile.activePage&&l(a.mobile.activePage)||t.hrefNoHash);if(f.data&&f.type==="get")m=j.addSearchParams(m,f.data),f.data=e;if(f.data&&f.type==="post")f.reloadPage=true;var s=j.getFilePath(m),p=j.convertUrlToDataUrl(m);f.pageContainer=f.pageContainer||a.mobile.pageContainer;h=f.pageContainer.children(":jqmData(url='"+p+"')");h.length===0&&p&&!j.isPath(p)&&(h=f.pageContainer.children("#"+
p).attr("data-"+a.mobile.ns+"url",p));if(h.length===0)if(a.mobile.firstPage&&j.isFirstPageUrl(s))a.mobile.firstPage.parent().length&&(h=a(a.mobile.firstPage));else if(j.isEmbeddedPage(s))return d.reject(m,c),d.promise();u&&u.reset();if(h.length){if(!f.reloadPage)return i(h,f.role),d.resolve(m,c,h),d.promise();g=h}var n=f.pageContainer,k=new a.Event("pagebeforeload"),q={url:b,absUrl:m,dataUrl:p,deferred:d,options:f};n.trigger(k,q);if(k.isDefaultPrevented())return d.promise();if(f.showLoadMsg)var v=
setTimeout(function(){a.mobile.showPageLoadingMsg()},f.loadMsgDelay);!a.mobile.allowCrossDomainPages&&!j.isSameDomain(x,m)?d.reject(m,c):a.ajax({url:s,type:f.type,data:f.data,dataType:"html",success:function(e,n,k){var o=a("<div></div>"),l=e.match(/<title[^>]*>([^<]*)/)&&RegExp.$1,t=RegExp("\\bdata-"+a.mobile.ns+"url=[\"']?([^\"'>]*)[\"']?");RegExp("(<[^>]+\\bdata-"+a.mobile.ns+"role=[\"']?page[\"']?[^>]*>)").test(e)&&RegExp.$1&&t.test(RegExp.$1)&&RegExp.$1&&(b=s=j.getFilePath(RegExp.$1));u&&u.set(s);
o.get(0).innerHTML=e;h=o.find(":jqmData(role='page'), :jqmData(role='dialog')").first();h.length||(h=a("<div data-"+a.mobile.ns+"role='page'>"+e.split(/<\/?body[^>]*>/gmi)[1]+"</div>"));l&&!h.jqmData("title")&&(~l.indexOf("&")&&(l=a("<div>"+l+"</div>").text()),h.jqmData("title",l));if(!a.support.dynamicBaseTag){var x=j.get(s);h.find("[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]").each(function(){var b=a(this).is("[href]")?"href":a(this).is("[src]")?"src":"action",c=a(this).attr(b),
c=c.replace(location.protocol+"//"+location.host+location.pathname,"");/^(\w+:|#|\/)/.test(c)||a(this).attr(b,x+c)})}h.attr("data-"+a.mobile.ns+"url",j.convertUrlToDataUrl(s)).attr("data-"+a.mobile.ns+"external-page",true).appendTo(f.pageContainer);h.one("pagecreate",a.mobile._bindPageRemove);i(h,f.role);m.indexOf("&"+a.mobile.subPageUrlKey)>-1&&(h=f.pageContainer.children(":jqmData(url='"+p+"')"));f.showLoadMsg&&(clearTimeout(v),a.mobile.hidePageLoadingMsg());q.xhr=k;q.textStatus=n;q.page=h;f.pageContainer.trigger("pageload",
q);d.resolve(m,c,h,g)},error:function(b,e,h){u&&u.set(j.get());q.xhr=b;q.textStatus=e;q.errorThrown=h;b=new a.Event("pageloadfailed");f.pageContainer.trigger(b,q);b.isDefaultPrevented()||(f.showLoadMsg&&(clearTimeout(v),a.mobile.hidePageLoadingMsg(),a("<div class='ui-loader ui-overlay-shadow ui-body-e ui-corner-all'><h1>"+a.mobile.pageLoadErrorMessage+"</h1></div>").css({display:"block",opacity:0.96,top:o.scrollTop()+100}).appendTo(f.pageContainer).delay(800).fadeOut(400,function(){a(this).remove()})),
d.reject(m,c))}});return d.promise()};a.mobile.loadPage.defaults={type:"get",data:e,reloadPage:false,role:e,showLoadMsg:false,pageContainer:e,loadMsgDelay:50};a.mobile.changePage=function(b,h){if(z)A.unshift(arguments);else{var g=a.extend({},a.mobile.changePage.defaults,h);g.pageContainer=g.pageContainer||a.mobile.pageContainer;g.fromPage=g.fromPage||a.mobile.activePage;var p=g.pageContainer,k=new a.Event("pagebeforechange"),q={toPage:b,options:g};p.trigger(k,q);if(!k.isDefaultPrevented())if(b=q.toPage,
z=true,typeof b=="string")a.mobile.loadPage(b,g).done(function(b,c,d,f){z=false;c.duplicateCachedPage=f;a.mobile.changePage(d,c)}).fail(function(){z=false;d(true);f();g.pageContainer.trigger("pagechangefailed",q)});else{if(b[0]===a.mobile.firstPage[0]&&!g.dataUrl)g.dataUrl=x.hrefNoHash;var k=g.fromPage,l=g.dataUrl&&j.convertUrlToDataUrl(g.dataUrl)||b.jqmData("url"),v=l;j.getFilePath(l);var o=n.getActive(),t=n.activeIndex===0,w=0,u=document.title,y=g.role==="dialog"||b.jqmData("role")==="dialog";if(k&&
k[0]===b[0]&&!g.allowSamePageTransition)z=false,p.trigger("pagechange",q);else{i(b,g.role);g.fromHashChange&&n.directHashChange({currentUrl:l,isBack:function(){w=-1},isForward:function(){w=1}});try{document.activeElement&&document.activeElement.nodeName.toLowerCase()!="body"?a(document.activeElement).blur():a("input:focus, textarea:focus, select:focus").blur()}catch(B){}y&&o&&(l=(o.url||"")+s);if(g.changeHash!==false&&l)n.ignoreNextHashChange=true,j.set(l);var C=!o?u:b.jqmData("title")||b.children(":jqmData(role='header')").find(".ui-title").getEncodedText();
C&&u==document.title&&(u=C);b.jqmData("title")||b.jqmData("title",u);g.transition=g.transition||(w&&!t?o.transition:e)||(y?a.mobile.defaultDialogTransition:a.mobile.defaultPageTransition);w||n.addNew(l,g.transition,u,v,g.role);document.title=n.getActive().title;a.mobile.activePage=b;g.reverse=g.reverse||w<0;c(b,k,g.transition,g.reverse).done(function(){d();g.duplicateCachedPage&&g.duplicateCachedPage.remove();m.removeClass("ui-mobile-rendering");f();p.trigger("pagechange",q)})}}}};a.mobile.changePage.defaults=
{transition:e,reverse:false,changeHash:true,fromHashChange:false,role:e,duplicateCachedPage:e,pageContainer:e,showLoadMsg:true,dataUrl:e,fromPage:e,allowSamePageTransition:false};a.mobile._registerInternalEvents=function(){a("form").live("submit",function(b){var c=a(this);if(a.mobile.ajaxEnabled&&!c.is(":jqmData(ajax='false')")){var d=c.attr("method"),f=c.attr("target"),e=c.attr("action");if(!e&&(e=l(c),e===t.hrefNoHash))e=x.hrefNoSearch;e=j.makeUrlAbsolute(e,l(c));!j.isExternal(e)&&!f&&(a.mobile.changePage(e,
{type:d&&d.length&&d.toLowerCase()||"get",data:c.serialize(),transition:c.jqmData("transition"),direction:c.jqmData("direction"),reloadPage:true}),b.preventDefault())}});a(document).bind("vclick",function(b){if(!(b.which>1)&&a.mobile.linkBindingEnabled&&(b=k(b.target))&&j.parseUrl(b.getAttribute("href")||"#").hash!=="#")d(true),q=a(b).closest(".ui-btn").not(".ui-disabled"),q.addClass(a.mobile.activeBtnClass),a("."+a.mobile.activePageClass+" .ui-btn").not(b).blur()});a(document).bind("click",function(b){if(a.mobile.linkBindingEnabled){var c=
k(b.target);if(c&&!(b.which>1)){var f=a(c),h=function(){window.setTimeout(function(){d(true)},200)};if(f.is(":jqmData(rel='back')"))return window.history.back(),false;var g=l(f),c=j.makeUrlAbsolute(f.attr("href")||"#",g);if(!a.mobile.ajaxEnabled&&!j.isEmbeddedPage(c))h();else{if(c.search("#")!=-1)if(c=c.replace(/[^#]*#/,""))c=j.isPath(c)?j.makeUrlAbsolute(c,g):j.makeUrlAbsolute("#"+c,x.hrefNoHash);else{b.preventDefault();return}var g=f.is("[rel='external']")||f.is(":jqmData(ajax='false')")||f.is("[target]"),
i=a.mobile.allowCrossDomainPages&&x.protocol==="file:"&&c.search(/^https?:/)!=-1;g||j.isExternal(c)&&!i?h():(h=f.jqmData("transition"),g=(g=f.jqmData("direction"))&&g==="reverse"||f.jqmData("back"),f=f.attr("data-"+a.mobile.ns+"rel")||e,a.mobile.changePage(c,{transition:h,reverse:g,role:f}),b.preventDefault())}}}});a(".ui-page").live("pageshow.prefetch",function(){var b=[];a(this).find("a:jqmData(prefetch)").each(function(){var c=a(this),f=c.attr("href");f&&a.inArray(f,b)===-1&&(b.push(f),a.mobile.loadPage(f,
{role:c.attr("data-"+a.mobile.ns+"rel")}))})});a.mobile._handleHashChange=function(b){var c=j.stripHash(b),f={transition:a.mobile.urlHistory.stack.length===0?"none":e,changeHash:false,fromHashChange:true};if(!a.mobile.hashListeningEnabled||n.ignoreNextHashChange)n.ignoreNextHashChange=false;else{if(n.stack.length>1&&c.indexOf(s)>-1)if(a.mobile.activePage.is(".ui-dialog"))n.directHashChange({currentUrl:c,either:function(b){var d=a.mobile.urlHistory.getActive();c=d.pageUrl;a.extend(f,{role:d.role,transition:d.transition,
reverse:b})}});else{n.directHashChange({currentUrl:c,isBack:function(){window.history.back()},isForward:function(){window.history.forward()}});return}c?(c=typeof c==="string"&&!j.isPath(c)?j.makeUrlAbsolute("#"+c,t):c,a.mobile.changePage(c,f)):a.mobile.changePage(a.mobile.firstPage,f)}};o.bind("hashchange",function(){a.mobile._handleHashChange(location.hash)});a(document).bind("pageshow",g);a(window).bind("throttledresize",g)}})(jQuery);
(function(a,e){var b={},d=a(e),f=a.mobile.path.parseUrl(location.href);a.extend(b,{initialFilePath:f.pathname+f.search,initialHref:f.hrefNoHash,hashchangeFired:false,state:function(){return{hash:location.hash||"#"+b.initialFilePath,title:document.title,initialHref:b.initialHref}},resetUIKeys:function(b){var f="&"+a.mobile.subPageUrlKey,d=b.indexOf(a.mobile.dialogHashKey);d>-1?b=b.slice(0,d)+"#"+b.slice(d):b.indexOf(f)>-1&&(b=b.split(f).join("#"+f));return b},nextHashChangePrevented:function(c){a.mobile.urlHistory.ignoreNextHashChange=
c;b.onHashChangeDisabled=c},onHashChange:function(){if(!b.onHashChangeDisabled){var c,f;c=location.hash;var d=a.mobile.path.isPath(c),e=d?location.href:a.mobile.getDocumentUrl();c=d?c.replace("#",""):c;f=b.state();c=a.mobile.path.makeUrlAbsolute(c,e);d&&(c=b.resetUIKeys(c));history.replaceState(f,document.title,c)}},onPopState:function(c){var f=c.originalEvent.state;f&&(b.nextHashChangePrevented(true),setTimeout(function(){b.nextHashChangePrevented(false);a.mobile._handleHashChange(f.hash)},100))},
init:function(){d.bind("hashchange",b.onHashChange);d.bind("popstate",b.onPopState);location.hash===""&&history.replaceState(b.state(),document.title,location.href)}});a(function(){a.mobile.pushStateEnabled&&a.support.pushState&&b.init()})})(jQuery,this);
(function(a){function e(b,d,f,c){var e=new a.Deferred,g=d?" reverse":"",i="ui-mobile-viewport-transitioning viewport-"+b;f.animationComplete(function(){f.add(c).removeClass("out in reverse "+b);c&&c[0]!==f[0]&&c.removeClass(a.mobile.activePageClass);f.parent().removeClass(i);e.resolve(b,d,f,c)});f.parent().addClass(i);c&&c.addClass(b+" out"+g);f.addClass(a.mobile.activePageClass+" "+b+" in"+g);return e.promise()}a.mobile.css3TransitionHandler=e;if(a.mobile.defaultTransitionHandler===a.mobile.noneTransitionHandler)a.mobile.defaultTransitionHandler=
e})(jQuery,this);
(function(a){a.mobile.page.prototype.options.degradeInputs={color:false,date:false,datetime:false,"datetime-local":false,email:false,month:false,number:false,range:"number",search:"text",tel:false,time:false,url:false,week:false};a(document).bind("pagecreate create",function(e){var b=a(e.target).closest(':jqmData(role="page")').data("page"),d;if(b)d=b.options,a(e.target).find("input").not(b.keepNativeSelector()).each(function(){var b=a(this),c=this.getAttribute("type"),e=d.degradeInputs[c]||"text";
if(d.degradeInputs[c]){var g=a("<div>").html(b.clone()).html(),i=g.indexOf(" type=")>-1;b.replaceWith(g.replace(i?/\s+type=["']?\w+['"]?/:/\/?>/,' type="'+e+'" data-'+a.mobile.ns+'type="'+c+'"'+(i?"":">")))}})})})(jQuery);
(function(a,e){a.widget("mobile.dialog",a.mobile.widget,{options:{closeBtnText:"Close",overlayTheme:"a",initSelector:":jqmData(role='dialog')"},_create:function(){var b=this,d=this.element,f=a("<a href='#' data-"+a.mobile.ns+"icon='delete' data-"+a.mobile.ns+"iconpos='notext'>"+this.options.closeBtnText+"</a>");d.addClass("ui-overlay-"+this.options.overlayTheme);d.attr("role","dialog").addClass("ui-dialog").find(":jqmData(role='header')").addClass("ui-corner-top ui-overlay-shadow").prepend(f).end().find(":jqmData(role='content'),:jqmData(role='footer')").addClass("ui-overlay-shadow").last().addClass("ui-corner-bottom");
f.bind("vclick",function(){b.close()});d.bind("vclick submit",function(b){var b=a(b.target).closest(b.type==="vclick"?"a":"form"),f;b.length&&!b.jqmData("transition")&&(f=a.mobile.urlHistory.getActive()||{},b.attr("data-"+a.mobile.ns+"transition",f.transition||a.mobile.defaultDialogTransition).attr("data-"+a.mobile.ns+"direction","reverse"))}).bind("pagehide",function(){a(this).find("."+a.mobile.activeBtnClass).removeClass(a.mobile.activeBtnClass)})},close:function(){e.history.back()}});a(a.mobile.dialog.prototype.options.initSelector).live("pagecreate",
function(){a(this).dialog()})})(jQuery,this);
(function(a){a.mobile.page.prototype.options.backBtnText="Back";a.mobile.page.prototype.options.addBackBtn=false;a.mobile.page.prototype.options.backBtnTheme=null;a.mobile.page.prototype.options.headerTheme="a";a.mobile.page.prototype.options.footerTheme="a";a.mobile.page.prototype.options.contentTheme=null;a(":jqmData(role='page'), :jqmData(role='dialog')").live("pagecreate",function(){var e=a(this),b=e.data("page").options,d=e.jqmData("role"),f=b.theme;a(":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')",
this).each(function(){var c=a(this),e=c.jqmData("role"),g=c.jqmData("theme"),i=g||b.contentTheme||d==="dialog"&&f,k;c.addClass("ui-"+e);if(e==="header"||e==="footer"){var l=g||(e==="header"?b.headerTheme:b.footerTheme)||f;c.addClass("ui-bar-"+l).attr("role",e==="header"?"banner":"contentinfo");g=c.children("a");i=g.hasClass("ui-btn-left");k=g.hasClass("ui-btn-right");i=i||g.eq(0).not(".ui-btn-right").addClass("ui-btn-left").length;k||g.eq(1).addClass("ui-btn-right");b.addBackBtn&&e==="header"&&a(".ui-page").length>
1&&c.jqmData("url")!==a.mobile.path.stripHash(location.hash)&&!i&&a("<a href='#' class='ui-btn-left' data-"+a.mobile.ns+"rel='back' data-"+a.mobile.ns+"icon='arrow-l'>"+b.backBtnText+"</a>").attr("data-"+a.mobile.ns+"theme",b.backBtnTheme||l).prependTo(c);c.children("h1, h2, h3, h4, h5, h6").addClass("ui-title").attr({tabindex:"0",role:"heading","aria-level":"1"})}else e==="content"&&(i&&c.addClass("ui-body-"+i),c.attr("role","main"))})})})(jQuery);
(function(a){a.widget("mobile.collapsible",a.mobile.widget,{options:{expandCueText:" click to expand contents",collapseCueText:" click to collapse contents",collapsed:true,heading:"h1,h2,h3,h4,h5,h6,legend",theme:null,contentTheme:null,iconTheme:"d",initSelector:":jqmData(role='collapsible')"},_create:function(){var e=this.element,b=this.options,d=e.addClass("ui-collapsible"),f=e.children(b.heading).first(),c=d.wrapInner("<div class='ui-collapsible-content'></div>").find(".ui-collapsible-content"),
h=e.closest(":jqmData(role='collapsible-set')").addClass("ui-collapsible-set"),e=h.children(":jqmData(role='collapsible')");f.is("legend")&&(f=a("<div role='heading'>"+f.html()+"</div>").insertBefore(f),f.next().remove());if(h.length){if(!b.theme)b.theme=h.jqmData("theme");if(!b.contentTheme)b.contentTheme=h.jqmData("content-theme")}c.addClass(b.contentTheme?"ui-body-"+b.contentTheme:"");f.insertBefore(c).addClass("ui-collapsible-heading").append("<span class='ui-collapsible-heading-status'></span>").wrapInner("<a href='#' class='ui-collapsible-heading-toggle'></a>").find("a").first().buttonMarkup({shadow:false,
corners:false,iconPos:"left",icon:"plus",theme:b.theme});h.length?(h.jqmData("collapsiblebound")||h.jqmData("collapsiblebound",true).bind("expand",function(b){a(b.target).closest(".ui-collapsible").siblings(".ui-collapsible").trigger("collapse")}),e.first().find("a").first().addClass("ui-corner-top").find(".ui-btn-inner").addClass("ui-corner-top"),e.last().jqmData("collapsible-last",true).find("a").first().addClass("ui-corner-bottom").find(".ui-btn-inner").addClass("ui-corner-bottom"),d.jqmData("collapsible-last")&&
f.find("a").first().add(f.find(".ui-btn-inner")).addClass("ui-corner-bottom")):f.find("a").first().add(f.find(".ui-btn-inner")).addClass("ui-corner-top ui-corner-bottom");d.bind("expand collapse",function(e){if(!e.isDefaultPrevented()){e.preventDefault();var i=a(this),e=e.type==="collapse",k=b.contentTheme;f.toggleClass("ui-collapsible-heading-collapsed",e).find(".ui-collapsible-heading-status").text(e?b.expandCueText:b.collapseCueText).end().find(".ui-icon").toggleClass("ui-icon-minus",!e).toggleClass("ui-icon-plus",
e);i.toggleClass("ui-collapsible-collapsed",e);c.toggleClass("ui-collapsible-content-collapsed",e).attr("aria-hidden",e);if(k&&(!h.length||d.jqmData("collapsible-last")))f.find("a").first().add(f.find(".ui-btn-inner")).toggleClass("ui-corner-bottom",e),c.toggleClass("ui-corner-bottom",!e);c.trigger("updatelayout")}}).trigger(b.collapsed?"collapse":"expand");f.bind("click",function(a){var b=f.is(".ui-collapsible-heading-collapsed")?"expand":"collapse";d.trigger(b);a.preventDefault()})}});a(document).bind("pagecreate create",
function(e){a(a.mobile.collapsible.prototype.options.initSelector,e.target).collapsible()})})(jQuery);(function(a){a.fn.fieldcontain=function(){return this.addClass("ui-field-contain ui-body ui-br")};a(document).bind("pagecreate create",function(e){a(":jqmData(role='fieldcontain')",e.target).fieldcontain()})})(jQuery);
(function(a){a.fn.grid=function(e){return this.each(function(){var b=a(this),d=a.extend({grid:null},e),f=b.children(),c={solo:1,a:2,b:3,c:4,d:5},d=d.grid;if(!d)if(f.length<=5)for(var h in c)c[h]===f.length&&(d=h);else d="a";c=c[d];b.addClass("ui-grid-"+d);f.filter(":nth-child("+c+"n+1)").addClass("ui-block-a");c>1&&f.filter(":nth-child("+c+"n+2)").addClass("ui-block-b");c>2&&f.filter(":nth-child(3n+3)").addClass("ui-block-c");c>3&&f.filter(":nth-child(4n+4)").addClass("ui-block-d");c>4&&f.filter(":nth-child(5n+5)").addClass("ui-block-e")})}})(jQuery);
(function(a,e){a.widget("mobile.navbar",a.mobile.widget,{options:{iconpos:"top",grid:null,initSelector:":jqmData(role='navbar')"},_create:function(){var b=this.element,d=b.find("a"),f=d.filter(":jqmData(icon)").length?this.options.iconpos:e;b.addClass("ui-navbar").attr("role","navigation").find("ul").grid({grid:this.options.grid});f||b.addClass("ui-navbar-noicons");d.buttonMarkup({corners:false,shadow:false,iconpos:f});b.delegate("a","vclick",function(){d.not(".ui-state-persist").removeClass(a.mobile.activeBtnClass);
a(this).addClass(a.mobile.activeBtnClass)})}});a(document).bind("pagecreate create",function(b){a(a.mobile.navbar.prototype.options.initSelector,b.target).navbar()})})(jQuery);
(function(a){var e={};a.widget("mobile.listview",a.mobile.widget,{options:{theme:null,countTheme:"c",headerTheme:"b",dividerTheme:"b",splitIcon:"arrow-r",splitTheme:"b",inset:false,initSelector:":jqmData(role='listview')"},_create:function(){var a=this;a.element.addClass(function(d,f){return f+" ui-listview "+(a.options.inset?" ui-listview-inset ui-corner-all ui-shadow ":"")});a.refresh(true)},_removeCorners:function(a,d){a=a.add(a.find(".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb"));d==="top"?a.removeClass("ui-corner-top ui-corner-tr ui-corner-tl"):
d==="bottom"?a.removeClass("ui-corner-bottom ui-corner-br ui-corner-bl"):a.removeClass("ui-corner-top ui-corner-tr ui-corner-tl ui-corner-bottom ui-corner-br ui-corner-bl")},_refreshCorners:function(a){var d,f;this.options.inset&&(d=this.element.children("li"),f=a?d.not(".ui-screen-hidden"):d.filter(":visible"),this._removeCorners(d),d=f.first().addClass("ui-corner-top"),d.add(d.find(".ui-btn-inner").not(".ui-li-link-alt span:first-child")).addClass("ui-corner-top").end().find(".ui-li-link-alt, .ui-li-link-alt span:first-child").addClass("ui-corner-tr").end().find(".ui-li-thumb").not(".ui-li-icon").addClass("ui-corner-tl"),
f=f.last().addClass("ui-corner-bottom"),f.add(f.find(".ui-btn-inner")).find(".ui-li-link-alt").addClass("ui-corner-br").end().find(".ui-li-thumb").not(".ui-li-icon").addClass("ui-corner-bl"));a||this.element.trigger("updatelayout")},_findFirstElementByTagName:function(a,d,f,c){var e={};for(e[f]=e[c]=true;a;){if(e[a.nodeName])return a;a=a[d]}return null},_getChildrenByTagName:function(b,d,f){var c=[],e={};e[d]=e[f]=true;for(b=b.firstChild;b;)e[b.nodeName]&&c.push(b),b=b.nextSibling;return a(c)},_addThumbClasses:function(b){var d,
f,c=b.length;for(d=0;d<c;d++)f=a(this._findFirstElementByTagName(b[d].firstChild,"nextSibling","img","IMG")),f.length&&(f.addClass("ui-li-thumb"),a(this._findFirstElementByTagName(f[0].parentNode,"parentNode","li","LI")).addClass(f.is(".ui-li-icon")?"ui-li-has-icon":"ui-li-has-thumb"))},refresh:function(b){this.parentPage=this.element.closest(".ui-page");this._createSubPages();var d=this.options,f=this.element,c=f.jqmData("dividertheme")||d.dividerTheme,e=f.jqmData("splittheme"),g=f.jqmData("spliticon"),
i=this._getChildrenByTagName(f[0],"li","LI"),k=a.support.cssPseudoElement||!a.nodeName(f[0],"ol")?0:1,l={},o,m,p,j,q;k&&f.find(".ui-li-dec").remove();if(!d.theme)d.theme=a.mobile.getInheritedTheme(this.element,"c");for(var n=0,A=i.length;n<A;n++){o=i.eq(n);m="ui-li";if(b||!o.hasClass("ui-li"))p=o.jqmData("theme")||d.theme,j=this._getChildrenByTagName(o[0],"a","A"),j.length?(q=o.jqmData("icon"),o.buttonMarkup({wrapperEls:"div",shadow:false,corners:false,iconpos:"right",icon:j.length>1||q===false?false:
q||"arrow-r",theme:p}),q!=false&&j.length==1&&o.addClass("ui-li-has-arrow"),j.first().addClass("ui-link-inherit"),j.length>1&&(m+=" ui-li-has-alt",j=j.last(),q=e||j.jqmData("theme")||d.splitTheme,j.appendTo(o).attr("title",j.getEncodedText()).addClass("ui-li-link-alt").empty().buttonMarkup({shadow:false,corners:false,theme:p,icon:false,iconpos:false}).find(".ui-btn-inner").append(a(document.createElement("span")).buttonMarkup({shadow:true,corners:true,theme:q,iconpos:"notext",icon:g||j.jqmData("icon")||
d.splitIcon})))):o.jqmData("role")==="list-divider"?(m+=" ui-li-divider ui-btn ui-bar-"+c,o.attr("role","heading"),k&&(k=1)):m+=" ui-li-static ui-body-"+p;k&&m.indexOf("ui-li-divider")<0&&(p=o.is(".ui-li-static:first")?o:o.find(".ui-link-inherit"),p.addClass("ui-li-jsnumbering").prepend("<span class='ui-li-dec'>"+k++ +". </span>"));l[m]||(l[m]=[]);l[m].push(o[0])}for(m in l)a(l[m]).addClass(m).children(".ui-btn-inner").addClass(m);f.find("h1, h2, h3, h4, h5, h6").addClass("ui-li-heading").end().find("p, dl").addClass("ui-li-desc").end().find(".ui-li-aside").each(function(){var b=
a(this);b.prependTo(b.parent())}).end().find(".ui-li-count").each(function(){a(this).closest("li").addClass("ui-li-has-count")}).addClass("ui-btn-up-"+(f.jqmData("counttheme")||this.options.countTheme)+" ui-btn-corner-all");this._addThumbClasses(i);this._addThumbClasses(f.find(".ui-link-inherit"));this._refreshCorners(b)},_idStringEscape:function(a){return a.replace(/[^a-zA-Z0-9]/g,"-")},_createSubPages:function(){var b=this.element,d=b.closest(".ui-page"),f=d.jqmData("url"),c=f||d[0][a.expando],
h=b.attr("id"),g=this.options,i="data-"+a.mobile.ns,k=this,l=d.find(":jqmData(role='footer')").jqmData("id"),o;typeof e[c]==="undefined"&&(e[c]=-1);h=h||++e[c];a(b.find("li>ul, li>ol").toArray().reverse()).each(function(c){var d=a(this),e=d.attr("id")||h+"-"+c,c=d.parent(),k=a(d.prevAll().toArray().reverse()),k=k.length?k:a("<span>"+a.trim(c.contents()[0].nodeValue)+"</span>"),n=k.first().getEncodedText(),e=(f||"")+"&"+a.mobile.subPageUrlKey+"="+e,A=d.jqmData("theme")||g.theme,z=d.jqmData("counttheme")||
b.jqmData("counttheme")||g.countTheme;o=true;d.detach().wrap("<div "+i+"role='page' "+i+"url='"+e+"' "+i+"theme='"+A+"' "+i+"count-theme='"+z+"'><div "+i+"role='content'></div></div>").parent().before("<div "+i+"role='header' "+i+"theme='"+g.headerTheme+"'><div class='ui-title'>"+n+"</div></div>").after(l?a("<div "+i+"role='footer' "+i+"id='"+l+"'>"):"").parent().appendTo(a.mobile.pageContainer).page();d=c.find("a:first");d.length||(d=a("<a/>").html(k||n).prependTo(c.empty()));d.attr("href","#"+e)}).listview();
o&&d.is(":jqmData(external-page='true')")&&d.data("page").options.domCache===false&&d.unbind("pagehide.remove").bind("pagehide.remove",function(b,c){var e=c.nextPage;c.nextPage&&(e=e.jqmData("url"),e.indexOf(f+"&"+a.mobile.subPageUrlKey)!==0&&(k.childPages().remove(),d.remove()))})},childPages:function(){var b=this.parentPage.jqmData("url");return a(":jqmData(url^='"+b+"&"+a.mobile.subPageUrlKey+"')")}});a(document).bind("pagecreate create",function(b){a(a.mobile.listview.prototype.options.initSelector,
b.target).listview()})})(jQuery);
(function(a){a.mobile.listview.prototype.options.filter=false;a.mobile.listview.prototype.options.filterPlaceholder="Filter items...";a.mobile.listview.prototype.options.filterTheme="c";a.mobile.listview.prototype.options.filterCallback=function(a,b){return a.toLowerCase().indexOf(b)===-1};a(":jqmData(role='listview')").live("listviewcreate",function(){var e=a(this),b=e.data("listview");if(b.options.filter){var d=a("<form>",{"class":"ui-listview-filter ui-bar-"+b.options.filterTheme,role:"search"});
a("<input>",{placeholder:b.options.filterPlaceholder}).attr("data-"+a.mobile.ns+"type","search").jqmData("lastval","").bind("keyup change",function(){var d=a(this),c=this.value.toLowerCase(),h=null,h=d.jqmData("lastval")+"",g=false,i="";d.jqmData("lastval",c);i=c.substr(0,h.length-1).replace(h,"");h=c.length<h.length||i.length!=c.length-h.length?e.children():e.children(":not(.ui-screen-hidden)");if(c){for(var k=h.length-1;k>=0;k--)d=a(h[k]),i=d.jqmData("filtertext")||d.text(),d.is("li:jqmData(role=list-divider)")?
(d.toggleClass("ui-filter-hidequeue",!g),g=false):b.options.filterCallback(i,c)?d.toggleClass("ui-filter-hidequeue",true):g=true;h.filter(":not(.ui-filter-hidequeue)").toggleClass("ui-screen-hidden",false);h.filter(".ui-filter-hidequeue").toggleClass("ui-screen-hidden",true).toggleClass("ui-filter-hidequeue",false)}else h.toggleClass("ui-screen-hidden",false);b._refreshCorners()}).appendTo(d).textinput();a(this).jqmData("inset")&&d.addClass("ui-listview-filter-inset");d.bind("submit",function(){return false}).insertBefore(e)}})})(jQuery);
(function(a){a(document).bind("pagecreate create",function(e){a(":jqmData(role='nojs')",e.target).addClass("ui-nojs")})})(jQuery);
(function(a,e){a.widget("mobile.checkboxradio",a.mobile.widget,{options:{theme:null,initSelector:"input[type='checkbox'],input[type='radio']"},_create:function(){var b=this,d=this.element,f=d.closest("form,fieldset,:jqmData(role='page')").find("label[for='"+d[0].id+"']"),c=d.attr("type"),h=c+"-on",g=c+"-off",i=d.parents(":jqmData(type='horizontal')").length?e:g;if(!(c!=="checkbox"&&c!=="radio")){a.extend(this,{label:f,inputtype:c,checkedClass:"ui-"+h+(i?"":" "+a.mobile.activeBtnClass),uncheckedClass:"ui-"+
g,checkedicon:"ui-icon-"+h,uncheckedicon:"ui-icon-"+g});if(!this.options.theme)this.options.theme=this.element.jqmData("theme");f.buttonMarkup({theme:this.options.theme,icon:i,shadow:false});d.add(f).wrapAll("<div class='ui-"+c+"'></div>");f.bind({vmouseover:function(b){a(this).parent().is(".ui-disabled")&&b.stopPropagation()},vclick:function(a){if(d.is(":disabled"))a.preventDefault();else return b._cacheVals(),d.prop("checked",c==="radio"&&true||!d.prop("checked")),d.triggerHandler("click"),b._getInputSet().not(d).prop("checked",
false),b._updateAll(),false}});d.bind({vmousedown:function(){b._cacheVals()},vclick:function(){var c=a(this);c.is(":checked")?(c.prop("checked",true),b._getInputSet().not(c).prop("checked",false)):c.prop("checked",false);b._updateAll()},focus:function(){f.addClass("ui-focus")},blur:function(){f.removeClass("ui-focus")}});this.refresh()}},_cacheVals:function(){this._getInputSet().each(function(){var b=a(this);b.jqmData("cacheVal",b.is(":checked"))})},_getInputSet:function(){return this.inputtype==
"checkbox"?this.element:this.element.closest("form,fieldset,:jqmData(role='page')").find("input[name='"+this.element.attr("name")+"'][type='"+this.inputtype+"']")},_updateAll:function(){var b=this;this._getInputSet().each(function(){var d=a(this);(d.is(":checked")||b.inputtype==="checkbox")&&d.trigger("change")}).checkboxradio("refresh")},refresh:function(){var b=this.element,d=this.label,f=d.find(".ui-icon");a(b[0]).prop("checked")?(d.addClass(this.checkedClass).removeClass(this.uncheckedClass),
f.addClass(this.checkedicon).removeClass(this.uncheckedicon)):(d.removeClass(this.checkedClass).addClass(this.uncheckedClass),f.removeClass(this.checkedicon).addClass(this.uncheckedicon));b.is(":disabled")?this.disable():this.enable()},disable:function(){this.element.prop("disabled",true).parent().addClass("ui-disabled")},enable:function(){this.element.prop("disabled",false).parent().removeClass("ui-disabled")}});a(document).bind("pagecreate create",function(b){a.mobile.checkboxradio.prototype.enhanceWithin(b.target)})})(jQuery);
(function(a,e){a.widget("mobile.button",a.mobile.widget,{options:{theme:null,icon:null,iconpos:null,inline:null,corners:true,shadow:true,iconshadow:true,initSelector:"button, [type='button'], [type='submit'], [type='reset'], [type='image']"},_create:function(){var b=this.element,d=this.options,f,c;this.button=a("<div></div>").text(b.text()||b.val()).insertBefore(b).buttonMarkup({theme:d.theme,icon:d.icon,iconpos:d.iconpos,inline:d.inline,corners:d.corners,shadow:d.shadow,iconshadow:d.iconshadow}).append(b.addClass("ui-btn-hidden"));
d=b.attr("type");f=b.attr("name");d!=="button"&&d!=="reset"&&f&&b.bind("vclick",function(){c===e&&(c=a("<input>",{type:"hidden",name:b.attr("name"),value:b.attr("value")}).insertBefore(b),a(document).one("submit",function(){c.remove();c=e}))});this.refresh()},enable:function(){this.element.attr("disabled",false);this.button.removeClass("ui-disabled").attr("aria-disabled",false);return this._setOption("disabled",false)},disable:function(){this.element.attr("disabled",true);this.button.addClass("ui-disabled").attr("aria-disabled",
true);return this._setOption("disabled",true)},refresh:function(){var a=this.element;a.prop("disabled")?this.disable():this.enable();this.button.data("textWrapper").text(a.text()||a.val())}});a(document).bind("pagecreate create",function(b){a.mobile.button.prototype.enhanceWithin(b.target)})})(jQuery);
(function(a,e){a.widget("mobile.slider",a.mobile.widget,{options:{theme:null,trackTheme:null,disabled:false,initSelector:"input[type='range'], :jqmData(type='range'), :jqmData(role='slider')"},_create:function(){var b=this,d=this.element,f=a.mobile.getInheritedTheme(d,"c"),c=this.options.theme||f,h=this.options.trackTheme||f,g=d[0].nodeName.toLowerCase(),f=g=="select"?"ui-slider-switch":"",i=d.attr("id"),k=i+"-label",i=a("[for='"+i+"']").attr("id",k),l=function(){return g=="input"?parseFloat(d.val()):
d[0].selectedIndex},o=g=="input"?parseFloat(d.attr("min")):0,m=g=="input"?parseFloat(d.attr("max")):d.find("option").length-1,p=window.parseFloat(d.attr("step")||1),j=a("<div class='ui-slider "+f+" ui-btn-down-"+h+" ui-btn-corner-all' role='application'></div>"),q=a("<a href='#' class='ui-slider-handle'></a>").appendTo(j).buttonMarkup({corners:true,theme:c,shadow:true}).attr({role:"slider","aria-valuemin":o,"aria-valuemax":m,"aria-valuenow":l(),"aria-valuetext":l(),title:l(),"aria-labelledby":k});
a.extend(this,{slider:j,handle:q,dragging:false,beforeStart:null,userModified:false,mouseMoved:false});g=="select"&&(j.wrapInner("<div class='ui-slider-inneroffset'></div>"),q.addClass("ui-slider-handle-snapping"),d.find("option"),d.find("option").each(function(b){var c=!b?"b":"a",d=!b?"right":"left",b=!b?" ui-btn-down-"+h:" "+a.mobile.activeBtnClass;a("<div class='ui-slider-labelbg ui-slider-labelbg-"+c+b+" ui-btn-corner-"+d+"'></div>").prependTo(j);a("<span class='ui-slider-label ui-slider-label-"+
c+b+" ui-btn-corner-"+d+"' role='img'>"+a(this).getEncodedText()+"</span>").prependTo(q)}));i.addClass("ui-slider");d.addClass(g==="input"?"ui-slider-input":"ui-slider-switch").change(function(){b.mouseMoved||b.refresh(l(),true)}).keyup(function(){b.refresh(l(),true,true)}).blur(function(){b.refresh(l(),true)});a(document).bind("vmousemove",function(a){if(b.dragging)return b.mouseMoved=true,g==="select"&&q.removeClass("ui-slider-handle-snapping"),b.refresh(a),b.userModified=b.beforeStart!==d[0].selectedIndex,
false});j.bind("vmousedown",function(a){b.dragging=true;b.userModified=false;b.mouseMoved=false;if(g==="select")b.beforeStart=d[0].selectedIndex;b.refresh(a);return false});j.add(document).bind("vmouseup",function(){if(b.dragging)return b.dragging=false,g==="select"&&(q.addClass("ui-slider-handle-snapping"),b.mouseMoved?b.userModified?b.refresh(b.beforeStart==0?1:0):b.refresh(b.beforeStart):b.refresh(b.beforeStart==0?1:0)),b.mouseMoved=false});j.insertAfter(d);this.handle.bind("vmousedown",function(){a(this).focus()}).bind("vclick",
false);this.handle.bind("keydown",function(c){var d=l();if(!b.options.disabled){switch(c.keyCode){case a.mobile.keyCode.HOME:case a.mobile.keyCode.END:case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:if(c.preventDefault(),!b._keySliding)b._keySliding=true,a(this).addClass("ui-state-active")}switch(c.keyCode){case a.mobile.keyCode.HOME:b.refresh(o);break;case a.mobile.keyCode.END:b.refresh(m);
break;case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:b.refresh(d+p);break;case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:b.refresh(d-p)}}}).keyup(function(){if(b._keySliding)b._keySliding=false,a(this).removeClass("ui-state-active")});this.refresh(e,e,true)},refresh:function(a,d,f){(this.options.disabled||this.element.attr("disabled"))&&this.disable();var c=this.element,e,g=c[0].nodeName.toLowerCase(),i=g==="input"?parseFloat(c.attr("min")):
0,k=g==="input"?parseFloat(c.attr("max")):c.find("option").length-1;if(typeof a==="object"){if(!this.dragging||a.pageX<this.slider.offset().left-8||a.pageX>this.slider.offset().left+this.slider.width()+8)return;e=Math.round((a.pageX-this.slider.offset().left)/this.slider.width()*100)}else a==null&&(a=g==="input"?parseFloat(c.val()):c[0].selectedIndex),e=(parseFloat(a)-i)/(k-i)*100;if(!isNaN(e)&&(e<0&&(e=0),e>100&&(e=100),a=Math.round(e/100*(k-i))+i,a<i&&(a=i),a>k&&(a=k),this.handle.css("left",e+"%"),
this.handle.attr({"aria-valuenow":g==="input"?a:c.find("option").eq(a).attr("value"),"aria-valuetext":g==="input"?a:c.find("option").eq(a).getEncodedText(),title:a}),g==="select"&&(a===0?this.slider.addClass("ui-slider-switch-a").removeClass("ui-slider-switch-b"):this.slider.addClass("ui-slider-switch-b").removeClass("ui-slider-switch-a")),!f))f=false,g==="input"?(f=c.val()!==a,c.val(a)):(f=c[0].selectedIndex!==a,c[0].selectedIndex=a),!d&&f&&c.trigger("change")},enable:function(){this.element.attr("disabled",
false);this.slider.removeClass("ui-disabled").attr("aria-disabled",false);return this._setOption("disabled",false)},disable:function(){this.element.attr("disabled",true);this.slider.addClass("ui-disabled").attr("aria-disabled",true);return this._setOption("disabled",true)}});a(document).bind("pagecreate create",function(b){a.mobile.slider.prototype.enhanceWithin(b.target)})})(jQuery);
(function(a){a.widget("mobile.textinput",a.mobile.widget,{options:{theme:null,initSelector:"input[type='text'], input[type='search'], :jqmData(type='search'), input[type='number'], :jqmData(type='number'), input[type='password'], input[type='email'], input[type='url'], input[type='tel'], textarea, input[type='time'], input[type='date'], input[type='month'], input[type='week'], input[type='datetime'], input[type='datetime-local'], input[type='color'], input:not([type])"},_create:function(){var e=this.element,
b=this.options.theme||a.mobile.getInheritedTheme(this.element,"c"),d=" ui-body-"+b,f,c;a("label[for='"+e.attr("id")+"']").addClass("ui-input-text");f=e.addClass("ui-input-text ui-body-"+b);typeof e[0].autocorrect!=="undefined"&&!a.support.touchOverflow&&(e[0].setAttribute("autocorrect","off"),e[0].setAttribute("autocomplete","off"));e.is("[type='search'],:jqmData(type='search')")?(f=e.wrap("<div class='ui-input-search ui-shadow-inset ui-btn-corner-all ui-btn-shadow ui-icon-searchfield"+d+"'></div>").parent(),
c=a("<a href='#' class='ui-input-clear' title='clear text'>clear text</a>").tap(function(a){e.val("").focus();e.trigger("change");c.addClass("ui-input-clear-hidden");a.preventDefault()}).appendTo(f).buttonMarkup({icon:"delete",iconpos:"notext",corners:true,shadow:true}),b=function(){setTimeout(function(){c.toggleClass("ui-input-clear-hidden",!e.val())},0)},b(),e.bind("paste cut keyup focus change blur",b)):e.addClass("ui-corner-all ui-shadow-inset"+d);e.focus(function(){f.addClass("ui-focus")}).blur(function(){f.removeClass("ui-focus")});
if(e.is("textarea")){var h=function(){var a=e[0].scrollHeight;e[0].clientHeight<a&&e.height(a+15)},g;e.keyup(function(){clearTimeout(g);g=setTimeout(h,100)});a.trim(e.val())&&(a(window).load(h),a(document).one("pagechange",h))}},disable:function(){(this.element.attr("disabled",true).is("[type='search'],:jqmData(type='search')")?this.element.parent():this.element).addClass("ui-disabled")},enable:function(){(this.element.attr("disabled",false).is("[type='search'],:jqmData(type='search')")?this.element.parent():
this.element).removeClass("ui-disabled")}});a(document).bind("pagecreate create",function(e){a.mobile.textinput.prototype.enhanceWithin(e.target)})})(jQuery);
(function(a){var e=function(b){var d=b.selectID,f=b.label,c=b.select.closest(".ui-page"),e=a("<div>",{"class":"ui-selectmenu-screen ui-screen-hidden"}).appendTo(c),g=b._selectOptions(),i=b.isMultiple=b.select[0].multiple,k=d+"-button",l=d+"-menu",o=a("<div data-"+a.mobile.ns+"role='dialog' data-"+a.mobile.ns+"theme='"+b.options.theme+"' data-"+a.mobile.ns+"overlay-theme='"+b.options.overlayTheme+"'><div data-"+a.mobile.ns+"role='header'><div class='ui-title'>"+f.getEncodedText()+"</div></div><div data-"+
a.mobile.ns+"role='content'></div></div>").appendTo(a.mobile.pageContainer).page(),m=a("<div>",{"class":"ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all ui-body-"+b.options.overlayTheme+" "+a.mobile.defaultDialogTransition}).insertAfter(e),p=a("<ul>",{"class":"ui-selectmenu-list",id:l,role:"listbox","aria-labelledby":k}).attr("data-"+a.mobile.ns+"theme",b.options.theme).appendTo(m),j=a("<div>",{"class":"ui-header ui-bar-"+b.options.theme}).prependTo(m),q=a("<h1>",{"class":"ui-title"}).appendTo(j),
n=a("<a>",{text:b.options.closeText,href:"#","class":"ui-btn-left"}).attr("data-"+a.mobile.ns+"iconpos","notext").attr("data-"+a.mobile.ns+"icon","delete").appendTo(j).buttonMarkup(),A=o.find(".ui-content"),z=o.find(".ui-header a");a.extend(b,{select:b.select,selectID:d,buttonId:k,menuId:l,thisPage:c,menuPage:o,label:f,screen:e,selectOptions:g,isMultiple:i,theme:b.options.theme,listbox:m,list:p,header:j,headerTitle:q,headerClose:n,menuPageContent:A,menuPageClose:z,placeholder:"",build:function(){var b=
this;b.refresh();b.select.attr("tabindex","-1").focus(function(){a(this).blur();b.button.focus()});b.button.bind("vclick keydown",function(c){if(c.type=="vclick"||c.keyCode&&(c.keyCode===a.mobile.keyCode.ENTER||c.keyCode===a.mobile.keyCode.SPACE))b.open(),c.preventDefault()});b.list.attr("role","listbox").delegate(".ui-li>a","focusin",function(){a(this).attr("tabindex","0")}).delegate(".ui-li>a","focusout",function(){a(this).attr("tabindex","-1")}).delegate("li:not(.ui-disabled, .ui-li-divider)",
"click",function(c){var d=b.select[0].selectedIndex,f=b.list.find("li:not(.ui-li-divider)").index(this),e=b._selectOptions().eq(f)[0];e.selected=b.isMultiple?!e.selected:true;b.isMultiple&&a(this).find(".ui-icon").toggleClass("ui-icon-checkbox-on",e.selected).toggleClass("ui-icon-checkbox-off",!e.selected);(b.isMultiple||d!==f)&&b.select.trigger("change");b.isMultiple||b.close();c.preventDefault()}).keydown(function(b){var c=a(b.target),d=c.closest("li");switch(b.keyCode){case 38:return b=d.prev(),
b.length&&(c.blur().attr("tabindex","-1"),b.find("a").first().focus()),false;case 40:return b=d.next(),b.length&&(c.blur().attr("tabindex","-1"),b.find("a").first().focus()),false;case 13:case 32:return c.trigger("click"),false}});b.menuPage.bind("pagehide",function(){b.list.appendTo(b.listbox);b._focusButton();a.mobile._bindPageRemove.call(b.thisPage)});b.screen.bind("vclick",function(){b.close()});b.headerClose.click(function(){if(b.menuType=="overlay")return b.close(),false});b.thisPage.addDependents(this.menuPage)},
_isRebuildRequired:function(){var a=this.list.find("li");return this._selectOptions().text()!==a.text()},refresh:function(b){var c=this;this._selectOptions();this.selected();var d=this.selectedIndices();(b||this._isRebuildRequired())&&c._buildList();c.setButtonText();c.setButtonCount();c.list.find("li:not(.ui-li-divider)").removeClass(a.mobile.activeBtnClass).attr("aria-selected",false).each(function(b){a.inArray(b,d)>-1&&(b=a(this),b.attr("aria-selected",true),c.isMultiple?b.find(".ui-icon").removeClass("ui-icon-checkbox-off").addClass("ui-icon-checkbox-on"):
b.addClass(a.mobile.activeBtnClass))})},close:function(){if(!this.options.disabled&&this.isOpen)this.menuType=="page"?window.history.back():(this.screen.addClass("ui-screen-hidden"),this.listbox.addClass("ui-selectmenu-hidden").removeAttr("style").removeClass("in"),this.list.appendTo(this.listbox),this._focusButton()),this.isOpen=false},open:function(){if(!this.options.disabled){var b=this,c=b.list.parent().outerHeight(),d=b.list.parent().outerWidth(),f=a(".ui-page-active"),e=a.support.touchOverflow&&
a.mobile.touchOverflowEnabled,f=f.is(".ui-native-fixed")?f.find(".ui-content"):f;scrollTop=e?f.scrollTop():a(window).scrollTop();btnOffset=b.button.offset().top;screenHeight=window.innerHeight;screenWidth=window.innerWidth;b.button.addClass(a.mobile.activeBtnClass);setTimeout(function(){b.button.removeClass(a.mobile.activeBtnClass)},300);if(c>screenHeight-80||!a.support.scrollTop){b.thisPage.unbind("pagehide.remove");if(scrollTop==0&&btnOffset>screenHeight)b.thisPage.one("pagehide",function(){a(this).jqmData("lastScroll",
btnOffset)});b.menuPage.one("pageshow",function(){a(window).one("silentscroll",function(){b.list.find(a.mobile.activeBtnClass).focus()});b.isOpen=true});b.menuType="page";b.menuPageContent.append(b.list);b.menuPage.find("div .ui-title").text(b.label.text());a.mobile.changePage(b.menuPage,{transition:a.mobile.defaultDialogTransition})}else{b.menuType="overlay";b.screen.height(a(document).height()).removeClass("ui-screen-hidden");var f=btnOffset-scrollTop,h=scrollTop+screenHeight-btnOffset,g=c/2,e=
parseFloat(b.list.parent().css("max-width")),c=f>c/2&&h>c/2?btnOffset+b.button.outerHeight()/2-g:f>h?scrollTop+screenHeight-c-30:scrollTop+30;d<e?e=(screenWidth-d)/2:(e=b.button.offset().left+b.button.outerWidth()/2-d/2,e<30?e=30:e+d>screenWidth&&(e=screenWidth-d-30));b.listbox.append(b.list).removeClass("ui-selectmenu-hidden").css({top:c,left:e}).addClass("in");b.list.find(a.mobile.activeBtnClass).focus();b.isOpen=true}}},_buildList:function(){var b=this,c=this.options,d=this.placeholder,f=[],e=
[],h=b.isMultiple?"checkbox-off":"false";b.list.empty().filter(".ui-listview").listview("destroy");b.select.find("option").each(function(g){var j=a(this),i=j.parent(),m=j.getEncodedText(),p="<a href='#'>"+m+"</a>",k=[],n=[];i.is("optgroup")&&(i=i.attr("label"),a.inArray(i,f)===-1&&(e.push("<li data-"+a.mobile.ns+"role='list-divider'>"+i+"</li>"),f.push(i)));if(!this.getAttribute("value")||m.length==0||j.jqmData("placeholder"))c.hidePlaceholderMenuItems&&k.push("ui-selectmenu-placeholder"),d=b.placeholder=
m;this.disabled&&(k.push("ui-disabled"),n.push("aria-disabled='true'"));e.push("<li data-"+a.mobile.ns+"option-index='"+g+"' data-"+a.mobile.ns+"icon='"+h+"' class='"+k.join(" ")+"' "+n.join(" ")+">"+p+"</li>")});b.list.html(e.join(" "));b.list.find("li").attr({role:"option",tabindex:"-1"}).first().attr("tabindex","0");this.isMultiple||this.headerClose.hide();!this.isMultiple&&!d.length?this.header.hide():this.headerTitle.text(this.placeholder);b.list.listview()},_button:function(){return a("<a>",
{href:"#",role:"button",id:this.buttonId,"aria-haspopup":"true","aria-owns":this.menuId})}})};a("select").live("selectmenubeforecreate",function(){var b=a(this).data("selectmenu");b.options.nativeMenu||e(b)})})(jQuery);
(function(a){a.widget("mobile.selectmenu",a.mobile.widget,{options:{theme:null,disabled:false,icon:"arrow-d",iconpos:"right",inline:null,corners:true,shadow:true,iconshadow:true,menuPageTheme:"b",overlayTheme:"a",hidePlaceholderMenuItems:true,closeText:"Close",nativeMenu:true,initSelector:"select:not(:jqmData(role='slider'))"},_button:function(){return a("<div/>")},_setDisabled:function(a){this.element.attr("disabled",a);this.button.attr("aria-disabled",a);return this._setOption("disabled",a)},_focusButton:function(){var a=
this;setTimeout(function(){a.button.focus()},40)},_selectOptions:function(){return this.select.find("option")},_preExtension:function(){this.select=this.element.wrap("<div class='ui-select'>");this.selectID=this.select.attr("id");this.label=a("label[for='"+this.selectID+"']").addClass("ui-select");this.isMultiple=this.select[0].multiple;if(!this.options.theme)this.options.theme=a.mobile.getInheritedTheme(this.select,"c")},_create:function(){this._preExtension();this._trigger("beforeCreate");this.button=
this._button();var e=this,b=this.options,d=this.button.text(a(this.select[0].options.item(this.select[0].selectedIndex==-1?0:this.select[0].selectedIndex)).text()).insertBefore(this.select).buttonMarkup({theme:b.theme,icon:b.icon,iconpos:b.iconpos,inline:b.inline,corners:b.corners,shadow:b.shadow,iconshadow:b.iconshadow});b.nativeMenu&&window.opera&&window.opera.version&&this.select.addClass("ui-select-nativeonly");if(this.isMultiple)this.buttonCount=a("<span>").addClass("ui-li-count ui-btn-up-c ui-btn-corner-all").hide().appendTo(d.addClass("ui-li-has-count"));
(b.disabled||this.element.attr("disabled"))&&this.disable();this.select.change(function(){e.refresh()});this.build()},build:function(){var e=this;this.select.appendTo(e.button).bind("vmousedown",function(){e.button.addClass(a.mobile.activeBtnClass)}).bind("focus vmouseover",function(){e.button.trigger("vmouseover")}).bind("vmousemove",function(){e.button.removeClass(a.mobile.activeBtnClass)}).bind("change blur vmouseout",function(){e.button.trigger("vmouseout").removeClass(a.mobile.activeBtnClass)}).bind("change blur",
function(){e.button.removeClass("ui-btn-down-"+e.options.theme)})},selected:function(){return this._selectOptions().filter(":selected")},selectedIndices:function(){var a=this;return this.selected().map(function(){return a._selectOptions().index(this)}).get()},setButtonText:function(){var e=this,b=this.selected();this.button.find(".ui-btn-text").text(function(){return!e.isMultiple?b.text():b.length?b.map(function(){return a(this).text()}).get().join(", "):e.placeholder})},setButtonCount:function(){var a=
this.selected();this.isMultiple&&this.buttonCount[a.length>1?"show":"hide"]().text(a.length)},refresh:function(){this.setButtonText();this.setButtonCount()},open:a.noop,close:a.noop,disable:function(){this._setDisabled(true);this.button.addClass("ui-disabled")},enable:function(){this._setDisabled(false);this.button.removeClass("ui-disabled")}});a(document).bind("pagecreate create",function(e){a.mobile.selectmenu.prototype.enhanceWithin(e.target)})})(jQuery);
(function(a,e){function b(b){for(var c;b;){if((c=typeof b.className==="string"&&b.className.split(" "))&&a.inArray("ui-btn",c)>-1&&a.inArray("ui-disabled",c)<0)break;b=b.parentNode}return b}a.fn.buttonMarkup=function(b){for(var b=b||{},c=0;c<this.length;c++){var h=this.eq(c),g=h[0],i=a.extend({},a.fn.buttonMarkup.defaults,{icon:b.icon!==e?b.icon:h.jqmData("icon"),iconpos:b.iconpos!==e?b.iconpos:h.jqmData("iconpos"),theme:b.theme!==e?b.theme:h.jqmData("theme"),inline:b.inline!==e?b.inline:h.jqmData("inline"),
shadow:b.shadow!==e?b.shadow:h.jqmData("shadow"),corners:b.corners!==e?b.corners:h.jqmData("corners"),iconshadow:b.iconshadow!==e?b.iconshadow:h.jqmData("iconshadow")},b),k="ui-btn-inner",l,o,m=document.createElement(i.wrapperEls),p=document.createElement(i.wrapperEls),j=i.icon?document.createElement("span"):null;d&&d();if(!i.theme)i.theme=a.mobile.getInheritedTheme(h,"c");l="ui-btn ui-btn-up-"+i.theme;i.inline&&(l+=" ui-btn-inline");if(i.icon)i.icon="ui-icon-"+i.icon,i.iconpos=i.iconpos||"left",
o="ui-icon "+i.icon,i.iconshadow&&(o+=" ui-icon-shadow");i.iconpos&&(l+=" ui-btn-icon-"+i.iconpos,i.iconpos=="notext"&&!h.attr("title")&&h.attr("title",h.getEncodedText()));i.corners&&(l+=" ui-btn-corner-all",k+=" ui-btn-corner-all");i.shadow&&(l+=" ui-shadow");g.setAttribute("data-"+a.mobile.ns+"theme",i.theme);h.addClass(l);m.className=k;m.setAttribute("aria-hidden","true");p.className="ui-btn-text";m.appendChild(p);if(j)j.className=o,m.appendChild(j);for(;g.firstChild;)p.appendChild(g.firstChild);
g.appendChild(m);a.data(g,"textWrapper",a(p))}return this};a.fn.buttonMarkup.defaults={corners:true,shadow:true,iconshadow:true,inline:false,wrapperEls:"span"};var d=function(){a(document).bind({vmousedown:function(d){var d=b(d.target),c;d&&(d=a(d),c=d.attr("data-"+a.mobile.ns+"theme"),d.removeClass("ui-btn-up-"+c).addClass("ui-btn-down-"+c))},"vmousecancel vmouseup":function(d){var d=b(d.target),c;d&&(d=a(d),c=d.attr("data-"+a.mobile.ns+"theme"),d.removeClass("ui-btn-down-"+c).addClass("ui-btn-up-"+
c))},"vmouseover focus":function(d){var d=b(d.target),c;d&&(d=a(d),c=d.attr("data-"+a.mobile.ns+"theme"),d.removeClass("ui-btn-up-"+c).addClass("ui-btn-hover-"+c))},"vmouseout blur":function(d){var d=b(d.target),c;d&&(d=a(d),c=d.attr("data-"+a.mobile.ns+"theme"),d.removeClass("ui-btn-hover-"+c+" ui-btn-down-"+c).addClass("ui-btn-up-"+c))}});d=null};a(document).bind("pagecreate create",function(b){a(":jqmData(role='button'), .ui-bar > a, .ui-header > a, .ui-footer > a, .ui-bar > :jqmData(role='controlgroup') > a",
b.target).not(".ui-btn, :jqmData(role='none'), :jqmData(role='nojs')").buttonMarkup()})})(jQuery);
(function(a){a.fn.controlgroup=function(e){return this.each(function(){function b(a){a.removeClass("ui-btn-corner-all ui-shadow").eq(0).addClass(h[0]).end().last().addClass(h[1]).addClass("ui-controlgroup-last")}var d=a(this),f=a.extend({direction:d.jqmData("type")||"vertical",shadow:false,excludeInvisible:true},e),c=d.children("legend"),h=f.direction=="horizontal"?["ui-corner-left","ui-corner-right"]:["ui-corner-top","ui-corner-bottom"];d.find("input").first().attr("type");c.length&&(d.wrapInner("<div class='ui-controlgroup-controls'></div>"),
a("<div role='heading' class='ui-controlgroup-label'>"+c.html()+"</div>").insertBefore(d.children(0)),c.remove());d.addClass("ui-corner-all ui-controlgroup ui-controlgroup-"+f.direction);b(d.find(".ui-btn"+(f.excludeInvisible?":visible":"")));b(d.find(".ui-btn-inner"));f.shadow&&d.addClass("ui-shadow")})};a(document).bind("pagecreate create",function(e){a(":jqmData(role='controlgroup')",e.target).controlgroup({excludeInvisible:false})})})(jQuery);
(function(a){a(document).bind("pagecreate create",function(e){a(e.target).find("a").not(".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')").addClass("ui-link")})})(jQuery);
(function(a,e){a.fn.fixHeaderFooter=function(){return!a.support.scrollTop||a.support.touchOverflow&&a.mobile.touchOverflowEnabled?this:this.each(function(){var b=a(this);b.jqmData("fullscreen")&&b.addClass("ui-page-fullscreen");b.find(".ui-header:jqmData(position='fixed')").addClass("ui-header-fixed ui-fixed-inline fade");b.find(".ui-footer:jqmData(position='fixed')").addClass("ui-footer-fixed ui-fixed-inline fade")})};a.mobile.fixedToolbars=function(){function b(){!i&&g==="overlay"&&(h||a.mobile.fixedToolbars.hide(true),
a.mobile.fixedToolbars.startShowTimer())}function d(a){var b=0,c,d;if(a){d=document.body;c=a.offsetParent;for(b=a.offsetTop;a&&a!=d;){b+=a.scrollTop||0;if(a==c)b+=c.offsetTop,c=a.offsetParent;a=a.parentNode}}return b}function f(b){var c=a(window).scrollTop(),e=d(b[0]),f=b.css("top")=="auto"?0:parseFloat(b.css("top")),h=window.innerHeight,g=b.outerHeight(),i=b.parents(".ui-page:not(.ui-page-fullscreen)").length;return b.is(".ui-header-fixed")?(f=c-e+f,f<e&&(f=0),b.css("top",i?f:c)):b.css("top",i?c+
h-g-(e-f):c+h-g)}if(a.support.scrollTop&&(!a.support.touchOverflow||!a.mobile.touchOverflowEnabled)){var c,h,g="inline",i=false,k=null,l=false,o=true;a(function(){var c=a(document),d=a(window);c.bind("vmousedown",function(){o&&(k=g)}).bind("vclick",function(b){o&&!a(b.target).closest("a,input,textarea,select,button,label,.ui-header-fixed,.ui-footer-fixed").length&&!l&&(a.mobile.fixedToolbars.toggle(k),k=null)}).bind("silentscroll",b);(c.scrollTop()===0?d:c).bind("scrollstart",function(){l=true;k===
null&&(k=g);var b=k=="overlay";if(i=b||!!h)a.mobile.fixedToolbars.clearShowTimer(),b&&a.mobile.fixedToolbars.hide(true)}).bind("scrollstop",function(b){a(b.target).closest("a,input,textarea,select,button,label,.ui-header-fixed,.ui-footer-fixed").length||(l=false,i&&(a.mobile.fixedToolbars.startShowTimer(),i=false),k=null)});d.bind("resize updatelayout",b)});a(".ui-page").live("pagebeforeshow",function(b,d){var e=a(b.target).find(":jqmData(role='footer')"),h=e.data("id"),g=d.prevPage,g=g&&g.find(":jqmData(role='footer')"),
g=g.length&&g.jqmData("id")===h;h&&g&&(c=e,f(c.removeClass("fade in out").appendTo(a.mobile.pageContainer)))}).live("pageshow",function(){var b=a(this);c&&c.length&&setTimeout(function(){f(c.appendTo(b).addClass("fade"));c=null},500);a.mobile.fixedToolbars.show(true,this)});a(".ui-collapsible-contain").live("collapse expand",b);return{show:function(b,c){a.mobile.fixedToolbars.clearShowTimer();g="overlay";return(c?a(c):a.mobile.activePage?a.mobile.activePage:a(".ui-page-active")).children(".ui-header-fixed:first, .ui-footer-fixed:not(.ui-footer-duplicate):last").each(function(){var c=
a(this),e=a(window).scrollTop(),h=d(c[0]),g=window.innerHeight,i=c.outerHeight(),e=c.is(".ui-header-fixed")&&e<=h+i||c.is(".ui-footer-fixed")&&h<=e+g;c.addClass("ui-fixed-overlay").removeClass("ui-fixed-inline");!e&&!b&&c.animationComplete(function(){c.removeClass("in")}).addClass("in");f(c)})},hide:function(b){g="inline";return(a.mobile.activePage?a.mobile.activePage:a(".ui-page-active")).children(".ui-header-fixed:first, .ui-footer-fixed:not(.ui-footer-duplicate):last").each(function(){var c=a(this),
d=c.css("top"),d=d=="auto"?0:parseFloat(d);c.addClass("ui-fixed-inline").removeClass("ui-fixed-overlay");if(d<0||c.is(".ui-header-fixed")&&d!==0)b?c.css("top",0):c.css("top")!=="auto"&&parseFloat(c.css("top"))!==0&&c.animationComplete(function(){c.removeClass("out reverse").css("top",0)}).addClass("out reverse")})},startShowTimer:function(){a.mobile.fixedToolbars.clearShowTimer();var b=[].slice.call(arguments);h=setTimeout(function(){h=e;a.mobile.fixedToolbars.show.apply(null,b)},100)},clearShowTimer:function(){h&&
clearTimeout(h);h=e},toggle:function(b){b&&(g=b);return g==="overlay"?a.mobile.fixedToolbars.hide():a.mobile.fixedToolbars.show()},setTouchToggleEnabled:function(a){o=a}}}}();a(document).bind("pagecreate create",function(b){a(":jqmData(position='fixed')",b.target).length&&a(b.target).each(function(){if(!a.support.scrollTop||a.support.touchOverflow&&a.mobile.touchOverflowEnabled)return this;var b=a(this);b.jqmData("fullscreen")&&b.addClass("ui-page-fullscreen");b.find(".ui-header:jqmData(position='fixed')").addClass("ui-header-fixed ui-fixed-inline fade");
b.find(".ui-footer:jqmData(position='fixed')").addClass("ui-footer-fixed ui-fixed-inline fade")})})})(jQuery);
(function(a){a.mobile.touchOverflowEnabled=false;a.mobile.touchOverflowZoomEnabled=false;a(document).bind("pagecreate",function(e){a.support.touchOverflow&&a.mobile.touchOverflowEnabled&&(e=a(e.target),e.is(":jqmData(role='page')")&&e.each(function(){var b=a(this),d=b.find(":jqmData(role='header'), :jqmData(role='footer')").filter(":jqmData(position='fixed')"),e=b.jqmData("fullscreen"),c=d.length?b.find(".ui-content"):b;b.addClass("ui-mobile-touch-overflow");c.bind("scrollstop",function(){c.scrollTop()>
0&&window.scrollTo(0,a.mobile.defaultHomeScroll)});d.length&&(b.addClass("ui-native-fixed"),e&&(b.addClass("ui-native-fullscreen"),d.addClass("fade in"),a(document).bind("vclick",function(){d.removeClass("ui-native-bars-hidden").toggleClass("in out").animationComplete(function(){a(this).not(".in").addClass("ui-native-bars-hidden")})})))}))})})(jQuery);
(function(a,e){function b(){var b=a("meta[name='viewport']");b.length?b.attr("content",b.attr("content")+", user-scalable=no"):a("head").prepend("<meta>",{name:"viewport",content:"user-scalable=no"})}var d=a("html");a("head");var f=a(e);a(e.document).trigger("mobileinit");if(a.mobile.gradeA()){if(a.mobile.ajaxBlacklist)a.mobile.ajaxEnabled=false;d.addClass("ui-mobile ui-mobile-rendering");var c=a("<div class='ui-loader ui-body-a ui-corner-all'><span class='ui-icon ui-icon-loading spin'></span><h1></h1></div>");
a.extend(a.mobile,{showPageLoadingMsg:function(){if(a.mobile.loadingMessage){var b=a("."+a.mobile.activeBtnClass).first();c.find("h1").text(a.mobile.loadingMessage).end().appendTo(a.mobile.pageContainer).css({top:a.support.scrollTop&&f.scrollTop()+f.height()/2||b.length&&b.offset().top||100})}d.addClass("ui-loading")},hidePageLoadingMsg:function(){d.removeClass("ui-loading")},initializePage:function(){var b=a(":jqmData(role='page')");b.length||(b=a("body").wrapInner("<div data-"+a.mobile.ns+"role='page'></div>").children(0));
b.add(":jqmData(role='dialog')").each(function(){var b=a(this);b.jqmData("url")||b.attr("data-"+a.mobile.ns+"url",b.attr("id")||location.pathname+location.search)});a.mobile.firstPage=b.first();a.mobile.pageContainer=b.first().parent().addClass("ui-mobile-viewport");f.trigger("pagecontainercreate");a.mobile.showPageLoadingMsg();!a.mobile.hashListeningEnabled||!a.mobile.path.stripHash(location.hash)?a.mobile.changePage(a.mobile.firstPage,{transition:"none",reverse:true,changeHash:false,fromHashChange:true}):
f.trigger("hashchange",[true])}});a.support.touchOverflow&&a.mobile.touchOverflowEnabled&&!a.mobile.touchOverflowZoomEnabled&&b();a.mobile._registerInternalEvents();a(function(){e.scrollTo(0,1);a.mobile.defaultHomeScroll=!a.support.scrollTop||a(e).scrollTop()===1?0:1;a.mobile.autoInitializePage&&a.mobile.initializePage();f.load(a.mobile.silentScroll)})}})(jQuery,this);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10 -4
View File
@@ -27,7 +27,7 @@
{{pass}} {{pass}}
{{else:}} {{else:}}
{{qry=''}} {{qry=''}}
{{pass}} {{pass}}
{{pass}} {{pass}}
<tr> <tr>
<th style="font-size: 1.75em;"> <th style="font-size: 1.75em;">
@@ -37,13 +37,19 @@
{{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn")}} {{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn")}}
</td> </td>
</tr> </tr>
{{pass}}
</table>
{{pass}} {{pass}}
{{pass}}
</table>
{{elif request.function=='select':}} {{elif request.function=='select':}}
<h2>{{=XML(str(T("Database %s select"))%A(request.args[0],_href=URL('index'))) }} <h2>{{=XML(str(T("Database %s select"))%A(request.args[0],_href=URL('index'))) }}
</h2> </h2>
{{if tb:}}
<h3>{{=T('Traceback')}}</h3>
<pre>
{{=tb}}
</pre>
{{pass}}
{{if table:}} {{if table:}}
{{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn")}}<br/><br/> {{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn")}}<br/><br/>
<h3>{{=T("Rows in Table")}}</h3><br/> <h3>{{=T("Rows in Table")}}</h3><br/>
@@ -149,7 +155,7 @@
<h4>{{=T("RAM")}}</h4> <h4>{{=T("RAM")}}</h4>
<p>{{=T.M("Number of entries: **%s**", ram['entries'])}}</p> <p>{{=T.M("Number of entries: **%s**", ram['entries'])}}</p>
{{if ram['entries'] > 0:}} {{if ram['entries'] > 0:}}
<p>{{=T.M("Hit Ratio: **%(ratio)s%%** (**%(hits)s** %{hit(hits)} and **%(misses)s** %%{miss(misses)})", <p>{{=T.M("Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})",
dict( ratio=ram['ratio'], hits=ram['hits'], misses=ram['misses']))}} dict( ratio=ram['ratio'], hits=ram['hits'], misses=ram['misses']))}}
</p> </p>
<p> <p>
@@ -43,8 +43,7 @@ def file_create_form(location):
form=FORM(T("create file with filename:")," ", form=FORM(T("create file with filename:")," ",
INPUT(_type="text",_name="filename",requires=IS_NOT_EMPTY), INPUT(_type="text",_name="filename",requires=IS_NOT_EMPTY),
INPUT(_type="hidden",_name="location",_value=location), INPUT(_type="hidden",_name="location",_value=location),
INPUT(_type="hidden",_name="sender",_value=URL('design',args=app)), INPUT(_type="hidden",_name="sender",_value=URL('design',args=app)))
INPUT(_type="submit",_value=T("Create")),_action=URL('create_file'))
return form return form
def upload_plugin_form(app): def upload_plugin_form(app):
form=FORM(T("upload plugin file:")," ", form=FORM(T("upload plugin file:")," ",
@@ -207,7 +206,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
{{pass}} {{pass}}
</ul> </ul>
<div data-role="fieldcontain">{{=file_create_form('%s/static/' % app)}} <div data-role="fieldcontain">{{=file_create_form('%s/static/' % app)}}
{{=file_upload_form('%s/static/' % app)}}</div> {{#=file_upload_form('%s/static/' % app)}}</div>
</div> </div>
</div> </div>
@@ -225,7 +224,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
</span> </span>
{{pass}} {{pass}}
<div data-role="fieldcontain">{{=file_create_form('%s/modules/' % app)}} <div data-role="fieldcontain">{{=file_create_form('%s/modules/' % app)}}
{{=file_upload_form('%s/modules/' % app)}}</div> {{#=file_upload_form('%s/modules/' % app)}}</div>
</div> </div>
</div> </div>
@@ -9,8 +9,9 @@
<div><input type="hidden" name="send" value="{{=send}}"/></div> <div><input type="hidden" name="send" value="{{=send}}"/></div>
<table> <table>
<tr><td>{{=T('Administrator Password:')}}</td><td><input type="password" name="password" id="password"/></td></tr> <tr><td>{{=T('Administrator Password:')}}</td><td><input type="password" name="password" id="password"/></td></tr>
<tr><td></td><td><input type="submit" name="login" value="{{=T('Login')}}"/></td></tr> <tr><td></td><td><button name="login">{{=T('Login')}}</button></td></tr>
</table> </table>
<input type="hidden" name="is_mobile" value="true"/>
</form> </form>
</div> </div>
{{else:}} {{else:}}
@@ -9,7 +9,7 @@
Remove this if you use the .htaccess --> Remove this if you use the .htaccess -->
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>web2py mobile admin</title> <title>Web2py Mobile Admin</title>
<!-- http://dev.w3.org/html5/markup/meta.name.html --> <!-- http://dev.w3.org/html5/markup/meta.name.html -->
<meta name="application-name" content="{{=request.application}}" /> <meta name="application-name" content="{{=request.application}}" />
@@ -24,16 +24,17 @@
initial-scale = 1.0 retains dimensions instead of zooming out if page height > device height initial-scale = 1.0 retains dimensions instead of zooming out if page height > device height
maximum-scale = 1.0 retains dimensions instead of zooming in if page width < device width maximum-scale = 1.0 retains dimensions instead of zooming in if page width < device width
--> -->
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Place favicon.ico and apple-touch-icon.png in the root of your domain and delete these references --> <!-- Place favicon.ico and apple-touch-icon.png in the root of your domain and delete these references -->
<link rel="shortcut icon" href="{{=URL('static','favicon.ico')}}" type="image/x-icon"> <link rel="shortcut icon" href="{{=URL('static','favicon.ico')}}" type="image/x-icon">
<link rel="apple-touch-icon" href="{{=URL('static','favicon.png')}}"> <link rel="apple-touch-icon" href="{{=URL('static','favicon.png')}}">
<!-- For the less-enabled mobile browsers like Opera Mini --> <!-- For the less-enabled mobile browsers like Opera Mini -->
<link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.0.min.css')}}"> <link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.2.0.min.css')}}">
<!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects --> <!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects -->
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script> <script src="{{=URL('static','js/modernizr.custom.js')}}"></script!>
{{include 'web2py_ajax.html'}} {{include 'web2py_ajax.html'}}
@@ -60,7 +61,7 @@ $(document).bind("mobileinit", function(){
}); });
</script> </script>
<script src="{{=URL('static','plugin_jqmobile/jquery.mobile-1.0.min.js')}}"></script> <script src="{{=URL('static','plugin_jqmobile/jquery.mobile-1.2.0.min.js')}}"></script>
<style> <style>
.error { color: red; } .error { color: red; }
@@ -75,7 +76,7 @@ $(document).bind("mobileinit", function(){
<!--[if (gt IE 9)|!(IE)]><!--> <body> <!--<![endif]--> <!--[if (gt IE 9)|!(IE)]><!--> <body> <!--<![endif]-->
<div data-role="page"> <div data-role="page">
<div data-role="header" data-fullscreen="true" data-position="fixed"> <div data-role="header">
<h1>web2py mobile admin/{{block sectionclass}}{{end}}</h1> <h1>web2py mobile admin/{{block sectionclass}}{{end}}</h1>
{{block header}} {{block header}}
{{if 'auth' in globals():}} {{if 'auth' in globals():}}
@@ -85,7 +86,7 @@ $(document).bind("mobileinit", function(){
<a rel="external" href="{{=URL('default','user',args='logout')}}" data-icon="logout" class="ui-btn-left">Logout</a> <a rel="external" href="{{=URL('default','user',args='logout')}}" data-icon="logout" class="ui-btn-left">Logout</a>
{{pass}} {{pass}}
{{pass}} {{pass}}
<a rel="external" href="{{=URL('default','index')}}" data-icon="home" class="ui-btn-right">{{=T("Home")}}</a> <a rel="external" href="{{=URL('default','index',vars=dict(is_mobile='true'))}}" data-icon="home" class="ui-btn-right">{{=T("Home")}}</a>
{{end}} {{end}}
</div> </div>
<div data-role="content"> <div data-role="content">
@@ -100,7 +101,7 @@ $(document).bind("mobileinit", function(){
{{pass}} {{pass}}
{{include}} {{include}}
</div> </div>
<div data-role="footer" data-fullscreen="true" data-position="fixed" style="padding: 5px; text-align: center"> <div data-role="footer">
{{block footer}} {{block footer}}
powered by web2py - @{{=request.now.year}} powered by web2py - @{{=request.now.year}}
{{end}} {{end}}
@@ -4,16 +4,16 @@
<h2>{{=T("Installed applications")}}</h2> <h2>{{=T("Installed applications")}}</h2>
<ul data-role="listview"> <ul data-role="listview" data-inset="true">
{{for a in apps:}} {{for a in apps:}}
<li> <li>
{{if a==request.application:}} {{if a==request.application:}}
<h3>{{=a}} ({{=T('currently running')}})</h3> <h3>{{=a}} ({{=T('currently running')}})</h3>
{{else:}} {{else:}}
<h3>{{=a}}</h3> <h3>{{=T("Application")}} {{=a}}</h3>
{{if MULTI_USER_MODE and db.app(name=a):}}<p>created by {{="%(first_name)s %(last_name)s" % db.auth_user[db.app(name=a).owner]}}</p>{{pass}} {{if MULTI_USER_MODE and db.app(name=a):}}<p>created by {{="%(first_name)s %(last_name)s" % db.auth_user[db.app(name=a).owner]}}</p>{{pass}}
{{pass}} {{pass}}
<ul> <ul data-role="listview" data-inset="true">
{{if a!=request.application:}} {{if a!=request.application:}}
{{=LI(A(T('Goto'),_rel="external",_href=URL(a,'default','index')))}} {{=LI(A(T('Goto'),_rel="external",_href=URL(a,'default','index')))}}
{{if not os.path.exists('applications/%s/compiled' % a):}} {{if not os.path.exists('applications/%s/compiled' % a):}}
@@ -19,6 +19,10 @@
<script src="{{=cm}}/mode/css/css.js"></script> <script src="{{=cm}}/mode/css/css.js"></script>
<script src="{{=cm}}/mode/javascript/javascript.js"></script> <script src="{{=cm}}/mode/javascript/javascript.js"></script>
<script src="{{=cm}}/mode/htmlmixed/htmlmixed.js"></script> <script src="{{=cm}}/mode/htmlmixed/htmlmixed.js"></script>
<script src="{{=cm}}/lib/util/search.js"></script>
<script src="{{=cm}}/lib/util/searchcursor.js"></script>
<script src="{{=cm}}/lib/util/dialog.js"></script>
<link rel="stylesheet" href="{{=cm}}/lib/util/dialog.css">
<script src="{{=cm}}/emmet.min.js"></script> <script src="{{=cm}}/emmet.min.js"></script>
<script language="Javascript" type="text/javascript" src="{{=URL('static','js/ajax_editor.js')}}"></script> <script language="Javascript" type="text/javascript" src="{{=URL('static','js/ajax_editor.js')}}"></script>
{{elif TEXT_EDITOR == 'ace':}} {{elif TEXT_EDITOR == 'ace':}}
@@ -246,6 +250,11 @@ window.onload = function() {
<ul> <ul>
{{=shortcut('Ctrl+S', T('Save via Ajax'))}} {{=shortcut('Ctrl+S', T('Save via Ajax'))}}
{{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}} {{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}}
{{=shortcut('Ctrl-F / Cmd-F', T('Start searching'))}}
{{=shortcut('Ctrl-G / Cmd-G', T('Find Next'))}}
{{=shortcut('Shift-Ctrl-G / Shift-Cmd-G', T('Find Previous'))}}
{{=shortcut('Shift-Ctrl-F / Cmd-Option-F', T('Replace'))}}
{{=shortcut('Shift-Ctrl-R / Shift-Cmd-Option-F', T('Replace All'))}}
{{=shortcut('Tab', T('Expand Abbreviation'))}} {{=shortcut('Tab', T('Expand Abbreviation'))}}
</ul> </ul>
{{elif TEXT_EDITOR == 'codemirror':}} {{elif TEXT_EDITOR == 'codemirror':}}
@@ -253,6 +262,11 @@ window.onload = function() {
<ul> <ul>
{{=shortcut('Ctrl+S', T('Save via Ajax'))}} {{=shortcut('Ctrl+S', T('Save via Ajax'))}}
{{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}} {{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}}
{{=shortcut('Ctrl-F / Cmd-F', T('Start searching'))}}
{{=shortcut('Ctrl-G / Cmd-G', T('Find Next'))}}
{{=shortcut('Shift-Ctrl-G / Shift-Cmd-G', T('Find Previous'))}}
{{=shortcut('Shift-Ctrl-F / Cmd-Option-F', T('Replace'))}}
{{=shortcut('Shift-Ctrl-R / Shift-Cmd-Option-F', T('Replace All'))}}
</ul> </ul>
{{else:}} {{else:}}
<h3>{{=T("Key bindings")}}</h3> <h3>{{=T("Key bindings")}}</h3>
@@ -76,6 +76,9 @@
<p> <p>
{{=T("Running on %s", request.env.server_software)}} {{=T("Running on %s", request.env.server_software)}}
</p> </p>
{{if session.is_mobile=='auto':}}<p>
{{=A(T('Try the mobile interface'),_href=URL('plugin_jqmobile','about'))}}</p>
{{pass}}
</div> </div>
{{pass}} {{pass}}
<!-- MULTI_USER_INTERFACE --> <!-- MULTI_USER_INTERFACE -->
@@ -34,7 +34,7 @@
</style> </style>
</head> </head>
<body> <body>
<iframe src="{{=URL('index')}}"></iframe> <iframe src="{{=URL('default','index',vars=dict(is_mobile='true'))}}"></iframe>
<div id="about"> <div id="about">
<h1><a href="http://web2py.com">web2py</a> plugin</h1> <h1><a href="http://web2py.com">web2py</a> plugin</h1>
<h2>for <a href="http://jquerymobile.com/">jQuery Mobile</a></h2> <h2>for <a href="http://jquerymobile.com/">jQuery Mobile</a></h2>
@@ -24,17 +24,17 @@
initial-scale = 1.0 retains dimensions instead of zooming out if page height > device height initial-scale = 1.0 retains dimensions instead of zooming out if page height > device height
maximum-scale = 1.0 retains dimensions instead of zooming in if page width < device width maximum-scale = 1.0 retains dimensions instead of zooming in if page width < device width
--> -->
<!--meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;"--> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Place favicon.ico and apple-touch-icon.png in the root of your domain and delete these references --> <!-- Place favicon.ico and apple-touch-icon.png in the root of your domain and delete these references -->
<link rel="shortcut icon" href="{{=URL('static','favicon.ico')}}" type="image/x-icon"> <link rel="shortcut icon" href="{{=URL('static','favicon.ico')}}" type="image/x-icon">
<link rel="apple-touch-icon" href="{{=URL('static','favicon.png')}}"> <link rel="apple-touch-icon" href="{{=URL('static','favicon.png')}}">
<!-- For the less-enabled mobile browsers like Opera Mini --> <!-- For the less-enabled mobile browsers like Opera Mini -->
<link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.0.min.css')}}"> <link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.2.0.min.css')}}">
<!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects --> <!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects -->
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script> <script src="{{=URL('static','js/modernizr.custom.js')}}"></script!>
{{include 'web2py_ajax.html'}} {{include 'web2py_ajax.html'}}
@@ -61,7 +61,7 @@ $(document).bind("mobileinit", function(){
}); });
</script> </script>
<script src="{{=URL('static','plugin_jqmobile/jquery.mobile-1.0.min.js')}}"></script> <script src="{{=URL('static','plugin_jqmobile/jquery.mobile-1.2.0.min.js')}}"></script>
<style> <style>
.error { color: red; } .error { color: red; }
@@ -76,7 +76,7 @@ $(document).bind("mobileinit", function(){
<!--[if (gt IE 9)|!(IE)]><!--> <body> <!--<![endif]--> <!--[if (gt IE 9)|!(IE)]><!--> <body> <!--<![endif]-->
<div data-role="page"> <div data-role="page">
<div data-role="header" data-fullscreen="true" data-position="fixed"> <div data-role="header">
<h1>{{=response.title}}</h1> <h1>{{=response.title}}</h1>
{{block header}} {{block header}}
{{if 'auth' in globals():}} {{if 'auth' in globals():}}
@@ -101,7 +101,7 @@ $(document).bind("mobileinit", function(){
{{pass}} {{pass}}
{{include}} {{include}}
</div> </div>
<div data-role="footer" data-fullscreen="true" data-position="fixed" style="padding: 5px; text-align: center"> <div data-role="footer">
{{block footer}} {{block footer}}
powered by web2py - @{{=request.now.year}} powered by web2py - @{{=request.now.year}}
{{end}} {{end}}
@@ -113,7 +113,6 @@ def query_by_table_type(tablename,db,request=request):
# ## list all databases and tables # ## list all databases and tables
# ########################################################### # ###########################################################
def index(): def index():
return dict(databases=databases) return dict(databases=databases)
@@ -207,6 +206,7 @@ def select():
if match: if match:
table = match.group('table') table = match.group('table')
try: try:
tb = None
nrows = db(query).count() nrows = db(query).count()
if form.vars.update_check and form.vars.update_fields: if form.vars.update_check and form.vars.update_fields:
db(query).update(**eval_in_global_env('dict(%s)' db(query).update(**eval_in_global_env('dict(%s)'
@@ -221,6 +221,8 @@ def select():
else: else:
rows = db(query,ignore_common_filters=True).select(limitby=(start, stop)) rows = db(query,ignore_common_filters=True).select(limitby=(start, stop))
except Exception, e: except Exception, e:
import traceback
tb = traceback.format_exc()
(rows, nrows) = ([], 0) (rows, nrows) = ([], 0)
response.flash = DIV(T('Invalid Query'),PRE(str(e))) response.flash = DIV(T('Invalid Query'),PRE(str(e)))
# begin handle upload csv # begin handle upload csv
@@ -250,6 +252,7 @@ def select():
rows=rows, rows=rows,
query=request.vars.query, query=request.vars.query,
formcsv = formcsv, formcsv = formcsv,
tb = tb,
) )
@@ -38,7 +38,6 @@ def hello6():
def status(): def status():
""" page that shows internal status""" """ page that shows internal status"""
response.view = 'generic.html'
return dict(toolbar=response.toolbar()) return dict(toolbar=response.toolbar())
+10 -8
View File
@@ -10,7 +10,7 @@ h3 {font-size:2.00em}
h4 {font-size:1.50em} h4 {font-size:1.50em}
h5 {font-size:1.25em} h5 {font-size:1.25em}
h6 {font-size:1.12em} h6 {font-size:1.12em}
th,label {font-weight:bold; white-space:nowrap} th,label {font-weight:bold; white-space:nowrap;}
td,th {text-align:left; padding:2px 5px 2px 5px} td,th {text-align:left; padding:2px 5px 2px 5px}
th {vertical-align:middle; border-right:1px solid white} th {vertical-align:middle; border-right:1px solid white}
td {vertical-align:top} td {vertical-align:top}
@@ -69,7 +69,6 @@ fieldset {padding:16px; border-top:1px #DEDEDE solid}
fieldset legend {text-transform:uppercase; font-weight:bold; padding:4px 16px 4px 16px; background:#f1f1f1} fieldset legend {text-transform:uppercase; font-weight:bold; padding:4px 16px 4px 16px; background:#f1f1f1}
/* fix ie problem with menu */ /* fix ie problem with menu */
.ie-lte7 .topbar .container {z-index:2}
td.w2p_fw {padding-bottom:1px} td.w2p_fw {padding-bottom:1px}
td.w2p_fl,td.w2p_fw,td.w2p_fc {vertical-align:top} td.w2p_fl,td.w2p_fw,td.w2p_fc {vertical-align:top}
@@ -187,10 +186,7 @@ div.error {
.web2py_paginator {} .web2py_paginator {}
.web2py_grid {width:100%} .web2py_grid {width:100%}
.web2py_grid table {width:100%} .web2py_grid table {width:100%}
.web2py_grid tbody td { .web2py_grid tbody td {padding:2px 5px 2px 5px; vertical-align: middle;}
padding:2px 5px 2px 5px;
vertical-align:middle;
}
.web2py_grid thead th,.web2py_grid tfoot td { .web2py_grid thead th,.web2py_grid tfoot td {
background-color:#EAEAEA; background-color:#EAEAEA;
@@ -299,10 +295,16 @@ li.w2p_grid_breadcrumb_elem {
.web2py_console input, .web2py_console select, .web2py_console input, .web2py_console select,
.web2py_console a { margin: 2px; } .web2py_console a { margin: 2px; }
.ie9 #w2p_query_panel {padding-bottom:2px}
#wiki_page_body { #wiki_page_body {
width: 600px; width: 600px;
height: auto; height: auto;
min-height: 400px; min-height: 400px;
} }
/* fix some IE problems */
.ie-lte7 .topbar .container {z-index:2}
.ie-lte8 div.flash{ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#222222', endColorstr='#000000', GradientType=0 ); }
.ie-lte8 div.flash:hover {filter:alpha(opacity=25);}
.ie9 #w2p_query_panel {padding-bottom:2px}
+49 -14
View File
@@ -39,7 +39,7 @@ function web2py_ajax_init(target) {
function web2py_event_handlers() { function web2py_event_handlers() {
var doc = jQuery(document) var doc = jQuery(document)
doc.on('click', '.flash', function(e){jQuery(this).fadeOut('slow'); e.preventDefault();}); doc.on('click', '.flash', function(e){var t=jQuery(this); if(t.css('top')=='0px') t.slideUp('slow'); else t.fadeOut(); e.preventDefault();});
doc.on('keyup', 'input.integer', function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); doc.on('keyup', 'input.integer', function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();});
doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();}); doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();});
var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?"; var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?";
@@ -55,7 +55,7 @@ function web2py_event_handlers() {
jQuery(function() { jQuery(function() {
var flash = jQuery('.flash'); var flash = jQuery('.flash');
flash.hide(); flash.hide();
if(flash.html()) flash.slideDown(); if(flash.html()) flash.append('<span style="float:right;">&times;</span>').slideDown();
web2py_ajax_init(document); web2py_ajax_init(document);
web2py_event_handlers(); web2py_event_handlers();
}); });
@@ -67,20 +67,20 @@ function web2py_trap_form(action,target) {
form.submit(function(e){ form.submit(function(e){
jQuery('.flash').hide().html(''); jQuery('.flash').hide().html('');
web2py_ajax_page('post',action,form.serialize(),target); web2py_ajax_page('post',action,form.serialize(),target);
e.preventDefault(); e.preventDefault();
}); });
}); });
} }
function web2py_trap_link(target) { function web2py_trap_link(target) {
jQuery('#'+target+' a.w2p_trap').each(function(i){ jQuery('#'+target+' a.w2p_trap').each(function(i){
var link=jQuery(this); var link=jQuery(this);
link.click(function(e) { link.click(function(e) {
jQuery('.flash').hide().html(''); jQuery('.flash').hide().html('');
web2py_ajax_page('get',link.attr('href'),[],target); web2py_ajax_page('get',link.attr('href'),[],target);
e.preventDefault(); e.preventDefault();
}); });
}); });
} }
function web2py_ajax_page(method, action, data, target) { function web2py_ajax_page(method, action, data, target) {
@@ -101,9 +101,9 @@ function web2py_ajax_page(method, action, data, target) {
web2py_trap_link(target); web2py_trap_link(target);
web2py_ajax_init('#'+target); web2py_ajax_init('#'+target);
if(command) if(command)
eval(decodeURIComponent(command)); eval(decodeURIComponent(command));
if(flash) if(flash)
jQuery('.flash').html(decodeURIComponent(flash)).slideDown(); jQuery('.flash').html(decodeURIComponent(flash)).slideDown();
} }
}); });
} }
@@ -151,11 +151,11 @@ function web2py_component(action, target, timeout, times){
} }
} else { } else {
// run once (no timeout specified) // run once (no timeout specified)
element.reload_counter = Infinity; element.reload_counter = Infinity;
web2py_ajax_page('get', action, null, target); web2py_ajax_page('get', action, null, target);
} }); } } }); }
function web2py_comet(url,onmessage,onopen,onclose) { function web2py_websocket(url,onmessage,onopen,onclose) {
if ("WebSocket" in window) { if ("WebSocket" in window) {
var ws = new WebSocket(url); var ws = new WebSocket(url);
ws.onopen = onopen?onopen:(function(){}); ws.onopen = onopen?onopen:(function(){});
@@ -165,3 +165,38 @@ function web2py_comet(url,onmessage,onopen,onclose) {
} else return false; // not supported } else return false; // not supported
} }
function web2py_calc_entropy(mystring) {
//calculate a simple entropy for a given string
var csets = new Array(
'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'0123456789', '!@#$\%^&*()', '~`-_=+[]{}\|;:\'",.<>?/',
'0123456789abcdefghijklmnopqrstuvwxyz');
var score = 0, other = {}, seen = {}, lastset = null, mystringlist = mystring.split('');
for (var i=0;i<mystringlist.length;i++) { // classify this character
var c = mystringlist[i], inset=5;
for(var j = 0; j<csets.length; j++)
if (csets[j].indexOf(c) != -1) {inset = j; break;}
//calculate effect of character on alphabet size
if(!(inset in seen)) {seen[inset] = 1;score += csets[inset].length;}
else if (!(c in other)) {score += 1;other[c] = 1;}
if (inset != lastset) {score += 1;lastset = inset;}
}
var entropy = mystring.length*Math.log(score)/0.6931471805599453;
return Math.round(entropy*100)/100
}
function web2py_validate_entropy(myfield, req_entropy) {
var validator = function () {
var v = (web2py_calc_entropy(myfield.val())||0)/req_entropy;
var r=0,g=0,b=0,rs=function(x){return Math.round(x*15).toString(16)};
if(v<=0.5) {r=1.0; g=2.0*v;}
else {r=(1.0-2.0*(Math.max(v,0)-0.5)); g=1.0;}
var color = '#'+rs(r)+rs(g)+rs(b);
myfield.css('background-color',color);
entropy_callback = myfield.data('entropy_callback');
if(entropy_callback) entropy_callback(v);
}
if(!myfield.hasClass('entropy_check')) myfield.on('keyup', validator).on('keydown', validator).addClass('entropy_check');
}
+10 -4
View File
@@ -27,7 +27,7 @@
{{pass}} {{pass}}
{{else:}} {{else:}}
{{qry=''}} {{qry=''}}
{{pass}} {{pass}}
{{pass}} {{pass}}
<tr> <tr>
<th style="font-size: 1.75em;"> <th style="font-size: 1.75em;">
@@ -37,13 +37,19 @@
{{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn")}} {{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn")}}
</td> </td>
</tr> </tr>
{{pass}}
</table>
{{pass}} {{pass}}
{{pass}}
</table>
{{elif request.function=='select':}} {{elif request.function=='select':}}
<h2>{{=XML(str(T("Database %s select"))%A(request.args[0],_href=URL('index'))) }} <h2>{{=XML(str(T("Database %s select"))%A(request.args[0],_href=URL('index'))) }}
</h2> </h2>
{{if tb:}}
<h3>{{=T('Traceback')}}</h3>
<pre>
{{=tb}}
</pre>
{{pass}}
{{if table:}} {{if table:}}
{{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn")}}<br/><br/> {{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn")}}<br/><br/>
<h3>{{=T("Rows in Table")}}</h3><br/> <h3>{{=T("Rows in Table")}}</h3><br/>
@@ -149,7 +155,7 @@
<h4>{{=T("RAM")}}</h4> <h4>{{=T("RAM")}}</h4>
<p>{{=T.M("Number of entries: **%s**", ram['entries'])}}</p> <p>{{=T.M("Number of entries: **%s**", ram['entries'])}}</p>
{{if ram['entries'] > 0:}} {{if ram['entries'] > 0:}}
<p>{{=T.M("Hit Ratio: **%(ratio)s%%** (**%(hits)s** %{hit(hits)} and **%(misses)s** %%{miss(misses)})", <p>{{=T.M("Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})",
dict( ratio=ram['ratio'], hits=ram['hits'], misses=ram['misses']))}} dict( ratio=ram['ratio'], hits=ram['hits'], misses=ram['misses']))}}
</p> </p>
<p> <p>
@@ -80,8 +80,7 @@ Current version: {{="%s.%s.%s (%s) %s" % request.env.web2py_version}}</p>
<br/> <br/>
<a class="button" href="{{=URL('download')}}" style="width:90%">DOWNLOAD NOW</a><br/> <a class="button" href="{{=URL('download')}}" style="width:90%">DOWNLOAD NOW</a><br/>
<a class="button" href="http://web2py.com/demo_admin" style="width:90%">ONLINE DEMO</a><br/> <a class="button" href="http://web2py.com/demo_admin" style="width:90%">ONLINE DEMO</a><br/>
<a class="button" href="http://web2py.com/poweredby" style="width:90%">SITES POWERED BY WEB2PY</a> <a class="button" href="http://web2py.com/poweredby" style="width:90%">SITES POWERED BY WEB2PY</a><br/>
<br/> <a class="button" href="http://www.chipin.com/contribute/id/dcd384d58839aa4d" style="width:90%">SUPPORT/DONATE</a></br>
<object width="234" height="60"><param name="movie" value="http://widget.chipin.com/widget/id/dcd384d58839aa4d"></param><param name="allowScriptAccess" value="always"></param><param name="wmode" value="transparent"></param><param name="color_scheme" value="gray"></param><embed src="http://widget.chipin.com/widget/id/dcd384d58839aa4d" flashVars="color_scheme=gray" type="application/x-shockwave-flash" allowScriptAccess="always" wmode="transparent" width="234" height="60"></embed></object>
</div> </div>
{{end}} {{end}}
@@ -70,6 +70,7 @@
</li><li>Ian Reinhart Geiser (html helpers) </li><li>Ian Reinhart Geiser (html helpers)
</li><li>Ionel Anton (Romanian translation) </li><li>Ionel Anton (Romanian translation)
</li><li>Jan Beilicke (markmin) </li><li>Jan Beilicke (markmin)
</li><li>Jeremy Dillworth
</li><li>Jonathan Benn (is_url validator and tests) </li><li>Jonathan Benn (is_url validator and tests)
</li><li>Jonathan Lundell (multiple contributions) </li><li>Jonathan Lundell (multiple contributions)
</li><li>Josh Goldfoot (xaml/html sanitizer) </li><li>Josh Goldfoot (xaml/html sanitizer)
@@ -156,6 +157,7 @@
<li><a href="http://www.danga.com/memcached/">memcache</a> developed by Evan Martin</li> <li><a href="http://www.danga.com/memcached/">memcache</a> developed by Evan Martin</li>
<li><a href="http://jquery.com/">jQuery</a> developed by John Resig</li> <li><a href="http://jquery.com/">jQuery</a> developed by John Resig</li>
<li>A syntax highlighter inspired by the code of <a href="http://www.petersblog.org/node/763">Peter Wilkinson</a></li> <li>A syntax highlighter inspired by the code of <a href="http://www.petersblog.org/node/763">Peter Wilkinson</a></li>
<li><a href="https://github.com/jtauber/pyuca">pyUCA</a> developed by <a href="http://jtauber.com/blog/2006/01/27/python_unicode_collation_algorithm/">James Tauber</a></li>
</ul> </ul>
+4 -1
View File
@@ -113,7 +113,6 @@ def query_by_table_type(tablename,db,request=request):
# ## list all databases and tables # ## list all databases and tables
# ########################################################### # ###########################################################
def index(): def index():
return dict(databases=databases) return dict(databases=databases)
@@ -207,6 +206,7 @@ def select():
if match: if match:
table = match.group('table') table = match.group('table')
try: try:
tb = None
nrows = db(query).count() nrows = db(query).count()
if form.vars.update_check and form.vars.update_fields: if form.vars.update_check and form.vars.update_fields:
db(query).update(**eval_in_global_env('dict(%s)' db(query).update(**eval_in_global_env('dict(%s)'
@@ -221,6 +221,8 @@ def select():
else: else:
rows = db(query,ignore_common_filters=True).select(limitby=(start, stop)) rows = db(query,ignore_common_filters=True).select(limitby=(start, stop))
except Exception, e: except Exception, e:
import traceback
tb = traceback.format_exc()
(rows, nrows) = ([], 0) (rows, nrows) = ([], 0)
response.flash = DIV(T('Invalid Query'),PRE(str(e))) response.flash = DIV(T('Invalid Query'),PRE(str(e)))
# begin handle upload csv # begin handle upload csv
@@ -250,6 +252,7 @@ def select():
rows=rows, rows=rows,
query=request.vars.query, query=request.vars.query,
formcsv = formcsv, formcsv = formcsv,
tb = tb,
) )
@@ -5,6 +5,7 @@
'book': ['books'], 'book': ['books'],
'is': ['are'], 'is': ['are'],
'man': ['men'], 'man': ['men'],
'miss': ['misses'],
'person': ['people'], 'person': ['people'],
'quark': ['quarks'], 'quark': ['quarks'],
'shop': ['shops'], 'shop': ['shops'],
+1 -1
View File
@@ -11,7 +11,7 @@
if not request.env.web2py_runtime_gae: if not request.env.web2py_runtime_gae:
## if NOT running on Google App Engine use SQLite or other DB ## if NOT running on Google App Engine use SQLite or other DB
db = DAL('sqlite://storage.sqlite') db = DAL('sqlite://storage.sqlite')
else: else:
## connect to Google BigTable (optional 'google:datastore://namespace') ## connect to Google BigTable (optional 'google:datastore://namespace')
db = DAL('google:datastore') db = DAL('google:datastore')
+2 -2
View File
@@ -33,9 +33,9 @@ def _():
# shortcuts # shortcuts
app = request.application app = request.application
ctr = request.controller ctr = request.controller
# useful links to internal and external resources # useful links to internal and external resources
response.menu+=[ response.menu+=[
(SPAN('web2py',_style='color:yellow'),False, 'http://web2py.com', [ (SPAN('web2py',_class='highlighted'),False, 'http://web2py.com', [
(T('My Sites'),False,URL('admin','default','site')), (T('My Sites'),False,URL('admin','default','site')),
(T('This App'),False,URL('admin','default','design/%s' % app), [ (T('This App'),False,URL('admin','default','design/%s' % app), [
(T('Controller'),False, (T('Controller'),False,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10 -5
View File
@@ -61,7 +61,7 @@ input[type=text],input[type=password],select{width:300px; margin-right:5px}
border-top:1px #DEDEDE solid; border-top:1px #DEDEDE solid;
} }
.header { .header {
// background:<fill here for header image>; /* background:<fill here for header image>; */
} }
@@ -69,7 +69,6 @@ fieldset {padding:16px; border-top:1px #DEDEDE solid}
fieldset legend {text-transform:uppercase; font-weight:bold; padding:4px 16px 4px 16px; background:#f1f1f1} fieldset legend {text-transform:uppercase; font-weight:bold; padding:4px 16px 4px 16px; background:#f1f1f1}
/* fix ie problem with menu */ /* fix ie problem with menu */
.ie-lte7 .topbar .container {z-index:2}
td.w2p_fw {padding-bottom:1px} td.w2p_fw {padding-bottom:1px}
td.w2p_fl,td.w2p_fw,td.w2p_fc {vertical-align:top} td.w2p_fl,td.w2p_fw,td.w2p_fc {vertical-align:top}
@@ -183,7 +182,7 @@ div.error {
* will look better with the declarations below * will look better with the declarations below
* if needed to remove base.css consider keeping these following lines in some css file. * if needed to remove base.css consider keeping these following lines in some css file.
*/ */
// .web2py_table {border:1px solid #ccc} /* .web2py_table {border:1px solid #ccc} */
.web2py_paginator {} .web2py_paginator {}
.web2py_grid {width:100%} .web2py_grid {width:100%}
.web2py_grid table {width:100%} .web2py_grid table {width:100%}
@@ -296,10 +295,16 @@ li.w2p_grid_breadcrumb_elem {
.web2py_console input, .web2py_console select, .web2py_console input, .web2py_console select,
.web2py_console a { margin: 2px; } .web2py_console a { margin: 2px; }
.ie9 #w2p_query_panel {padding-bottom:2px}
#wiki_page_body { #wiki_page_body {
width: 600px; width: 600px;
height: auto; height: auto;
min-height: 400px; min-height: 400px;
} }
/* fix some IE problems */
.ie-lte7 .topbar .container {z-index:2}
.ie-lte8 div.flash{ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#222222', endColorstr='#000000', GradientType=0 ); }
.ie-lte8 div.flash:hover {filter:alpha(opacity=25);}
.ie9 #w2p_query_panel {padding-bottom:2px}
@@ -1,20 +1,19 @@
#navbar .auth_navbar, #navbar .auth_navbar a {color:inherit;} /*=============================================================
CUSTOM RULES
==============================================================*/
div.flash.flash-center { body{height:auto;} /* to avoid vertical scroll bar */
left: 25%; div.flash.flash-center{left:25%;right:25%;}
right: 25%; div.flash.flash-top,div.flash.flash-top:hover{
} position:relative;
display:block;
div.flash.flash-top, div.flash.flash-top:hover {
position: relative;
display: block;
margin:0; margin:0;
padding:1em; padding:1em;
top:0; top:0;
left:0; left:0;
width:100%; width:100%;
text-align:center; text-align:center;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);
color:#865100; color:#865100;
background:#feea9a; background:#feea9a;
border:1px solid; border:1px solid;
@@ -24,154 +23,209 @@ div.flash.flash-top, div.flash.flash-top:hover {
border-radius:0; border-radius:0;
opacity:1; opacity:1;
} }
#header{margin-top:60px;}
/* bootstrap dropdown */
.dropdown-menu ul {
left: 100%;
position: absolute;
top: 0;
visibility: hidden;
margin-top: -1px;
}
.dropdown-menu li:hover ul {
visibility: visible;
}
#header {
margin-top: 60px;
}
.mastheader h1 { .mastheader h1 {
margin-bottom: 9px; margin-bottom:9px;
font-size: 81px; font-size:81px;
font-weight: bold; font-weight:bold;
letter-spacing: -1px; letter-spacing:-1px;
line-height: 1; line-height:1;
font-size:54px;
} }
.mastheader h1 {
font-size: 54px;
}
.mastheader small { .mastheader small {
font-size: 20px; font-size:20px;
font-weight: 300; font-weight:300;
} }
/* auth navbar - primitive style */
.navbar .dropdown-menu ul:before { .auth_navbar,.auth_navbar a{color:inherit;}
border-bottom: 7px solid transparent; .ie-lte7 .auth_navbar,.auth_navbar a{color:expression(this.parentNode.currentStyle['color']); /* ie7 doesn't support inherit */}
border-left: none; .auth_navbar a{white-space:nowrap;} /* to avoid the nav split on more lines */
border-right: 7px solid rgba(0, 0, 0, 0.2); .auth_navbar a:hover{color:white;text-decoration:none;}
border-top: 7px solid transparent; ul#navbar>.auth_navbar{
left: -7px;
top: 5px;
}
.navbar .dropdown-menu ul:after {
border-top: 6px solid transparent;
border-left: none;
border-right: 6px solid #fff;
border-bottom: 6px solid transparent;
left: 10px;
top: 6px;
left: -6px;
}
.dropdown-menu span{
display:inline-block; display:inline-block;
padding:5px;
} }
/* form errors message box customization */
.chevron-right { div.error_wrapper{margin-bottom:9px;}
border-left: 4px solid #000; div.error_wrapper .error{
border-right: 4px solid transparent;
border-bottom: 4px solid transparent;
border-top: 4px solid transparent;
content: "";
display: inline-block;
height: 0;
opacity: 0.7;
vertical-align: top;
width: 0;
margin-right:-13px;
margin-top: 7px;
float:right;
}
.open > .dropdown-menu ul { /* fix menu issue when BS2.0.4 is applied */
display: block;
}
.ie-lte7 #navbar .auth_navbar, #navbar .auth_navbar a {color:expression(this.parentNode.currentStyle['color']); /* ie7 doesn't support inherit */}
#navbar .auth_navbar a:hover {color:white;text-decoration:none;} /* this overwrite bootswatch */
ul#navbar{padding:0;} /*reset ul padding */
ul#navbar>.auth_navbar{display:inline-block;padding:5px;} /* set padding of span.auth_navbar inside ul*/
body {
height:auto; /*to avoid vertical scroll bar*/
}
h1,h2,h3,h4,h5,h6 {font-family: inherit;}
li {margin-bottom: 0;} /*bootswatch*/
.auth_navbar, .navbar .btn-group { padding:0; }
[class^="icon-"],[class*=" icon-"]{background-image:url("../images/glyphicons-halflings.png")} /* right folder for bootstrap black images/icons */
.icon-white{background-image:url("../images/glyphicons-halflings-white.png");} /* right folder for bootstrap white images/icons */
.navbar-inner{
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
position:relative;
}
@media only screen and (max-width: 979px) {
body {
padding-top: 0px;
}
#navbar{bottom:-10px;left:4px;}
div.flash {
right: 5px;
}
.dropdown-menu ul {
visibility: visible;
}
}
div.error_wrapper {
margin-bottom: 9px;
}
div.error {
border-radius: 4px; border-radius: 4px;
-o-border-radius: 4px; -o-border-radius: 4px;
-moz-border-radius: 4px; -moz-border-radius: 4px;
-webkit-border-radius: 4px; -webkit-border-radius: 4px;
} }
/* below rules are only for formstyle = bootstrap
trying to make errors look like bootstrap ones */
div.controls .error_wrapper{
display:inline-block;
margin-bottom:0;
vertical-align:middle;
}
div.controls .error{
min-width:5px;
background:inherit;
color:#B94A48;
border:none;
padding:0;
margin:0;
//display:inline; /* uncommenting this, the animation effect is lost */
}
div.controls .inline-help{color:#3A87AD;}
div.controls .error_wrapper+.inline-help{margin-left:-99999px;}
/* beautify brand */
.navbar-inverse .brand{color:#c6cecc;}
.navbar-inverse .brand b{display:inline-block;margin-top:-1px;}
.navbar-inverse .brand b>span{font-size:22px;color:white}
.navbar-inverse .brand:hover b>span{color:white}
/* beautify web2py link in navbar */
span.highlighted{color:#d8d800;}
.open span.highlighted{color:#ffff00;}
input[type="button"], input[type="submit"] { /*=============================================================
// to be fixed OVERRIDING WEB2PY.CSS RULES
==============================================================*/
/* reset to default */
a{white-space:normal;}
li{margin-bottom:0;}
textarea,button{display:block;}
/*reset ul padding */
ul#navbar{padding:0;}
/* label aligned to related input */
td.w2p_fl,td.w2p_fc {padding:0;}
#web2py_user_form td{vertical-align:middle;}
/*=============================================================
OVERRIDING BOOTSTRAP.CSS RULES
==============================================================*/
/* because web2py handles this via js */
.hidden{visibility:visible;}
/* right folder for bootstrap black images/icons */
[class^="icon-"],[class*=" icon-"]{
background-image:url("../images/glyphicons-halflings.png")
}
/* right folder for bootstrap white images/icons */
.icon-white,
.nav-tabs > .active > a > [class^="icon-"],
.nav-tabs > .active > a > [class*=" icon-"],
.nav-pills > .active > a > [class^="icon-"],
.nav-pills > .active > a > [class*=" icon-"],
.nav-list > .active > a > [class^="icon-"],
.nav-list > .active > a > [class*=" icon-"],
.navbar-inverse .nav > .active > a > [class^="icon-"],
.navbar-inverse .nav > .active > a > [class*=" icon-"],
.dropdown-menu > li > a:hover > [class^="icon-"],
.dropdown-menu > li > a:hover > [class*=" icon-"],
.dropdown-menu > .active > a > [class^="icon-"],
.dropdown-menu > .active > a > [class*=" icon-"] {
background-image:url("../images/glyphicons-halflings-white.png");
}
/* bootstrap has a label as input's wrapper while web2py has a div */
div>input[type="radio"],div>input[type="checkbox"]{margin:0;}
/* bootstrap has button instead of input */
input[type="button"], input[type="submit"]{margin-right:8px;}
/*=============================================================
RULES FOR SOLVING CONFLICTS BETWEEN WEB2PY.CSS AND BOOTSTRAP.CSS
==============================================================*/
/*when formstyle=table3cols*/
tr#auth_user_remember__row>td.w2p_fw>div{padding-bottom:8px;}
td.w2p_fw div>label{vertical-align:middle;}
td.w2p_fc {padding-bottom:5px;}
/*when formstyle=divs*/
div#auth_user_remember__row{margin-top:4px;}
div#auth_user_remember__row>.w2p_fl{display:none;}
div#auth_user_remember__row>.w2p_fw{min-height:39px;}
div.w2p_fw,div.w2p_fc{
display:inline-block;
vertical-align:middle;
margin-bottom:0;
}
div.w2p_fc{
padding-left:5px;
margin-top:-8px;
}
/*when formstyle=ul*/
form>ul{
list-style:none;
margin:0;
}
li#auth_user_remember__row{margin-top:4px;}
li#auth_user_remember__row>.w2p_fl{display:none;}
li#auth_user_remember__row>.w2p_fw{min-height:39px;}
/*when formstyle=bootstrap*/
#auth_user_remember__row label.checkbox{display:block;}
span.inline-help{display:inline-block;}
input[type="text"].input-xlarge,input[type="password"].input-xlarge{width:270px;}
/*when recaptcha is used*/
#recaptcha{min-height:30px;display:inline-block;margin-bottom:0;line-height:30px;vertical-align:middle;}
td>#recaptcha{margin-bottom:6px;}
div>#recaptcha{margin-bottom:9px;}
div.control-group.error{
width:auto;
background:transparent;
border:0;
color:inherit;
padding:0;
background-repeat:repeat;
} }
@media only screen and (max-width: 479px) { /*=============================================================
body { OTHER RULES
padding-left: 10px; ==============================================================*/
padding-right: 10px;
.navbar-inner{
position:relative; /*unnecessary ??*/
}
/* Massimo Di Pierro fixed alignment in forms with list:string */
form table tr{margin-bottom:9px;}
td.w2p_fw ul{margin-left:0px;}
/* web2py_console in grid and smartgrid */
.hidden{visibility:visible;}
.web2py_console input{
display: inline-block;
margin-bottom: 0;
vertical-align: middle;
}
.web2py_console input[type="submit"],
.web2py_console input[type="button"],
.web2py_console button{
padding-top:4px;
padding-bottom:4px;
margin:3px 0 0 2px;
}
.web2py_console a,
.web2py_console select,
.web2py_console input
{
margin:3px 0 0 2px;
}
.web2py_grid form table{width:auto;}
/* auth_user_remember checkbox extrapadding in IE fix */
.ie-lte9 input#auth_user_remember.checkbox {padding-left:0;}
/*=============================================================
MEDIA QUERIES
==============================================================*/
@media only screen and (max-width:979px){
body{padding-top:0px;}
#navbar{bottom:-10px;left:4px;}
div.flash{right:5px;}
.dropdown-menu ul{visibility:visible;}
}
@media only screen and (max-width:479px){
body{
padding-left:10px;
padding-right:10px;
} }
.navbar-fixed-top,.navbar-fixed-bottom {
.navbar-fixed-top, .navbar-fixed-bottom { margin-left:-10px;
margin-left: -10px; margin-right:-10px;
margin-right: -10px;
} }
input[type="text"],input[type="password"],select{
input[type="text"], input[type="password"], select { width:95%;
width: 95%;
} }
} }
@@ -1,3 +1,23 @@
/*=============================================================
BOOTSTRAP DROPDOWN MENU
==============================================================*/
.dropdown-menu ul{
left:100%;
position:absolute;
top:0;
visibility:hidden;
margin-top:-1px;
}
.dropdown-menu li:hover ul{visibility:visible;}
.navbar .dropdown-menu ul:before{
border-bottom:7px solid transparent;
border-left:none;
border-right:7px solid rgba(0, 0, 0, 0.2);
border-top:7px solid transparent;
left:-7px;
top:5px;
}
.nav > li.dropdown > a:after { .nav > li.dropdown > a:after {
border-left: 4px solid transparent; border-left: 4px solid transparent;
border-right: 4px solid transparent; border-right: 4px solid transparent;
@@ -15,8 +35,7 @@
border-bottom-color: #FFFFFF; border-bottom-color: #FFFFFF;
border-top-color: #FFFFFF; border-top-color: #FFFFFF;
} }
.dropdown-menu span{display:inline-block;}
ul.dropdown-menu li.dropdown > a:after { ul.dropdown-menu li.dropdown > a:after {
border-left: 4px solid #000; border-left: 4px solid #000;
border-right: 4px solid transparent; border-right: 4px solid transparent;
@@ -35,4 +54,69 @@ ul.dropdown-menu li.dropdown > a:after {
ul.nav li.dropdown:hover ul.dropdown-menu { ul.nav li.dropdown:hover ul.dropdown-menu {
display: block; display: block;
} }
.open >.dropdown-menu ul{display:block;} /* fix menu issue when BS2.0.4 is applied */
/*=============================================================
BOOTSTRAP SUBMIT BUTTON
==============================================================*/
input[type='submit']:not(.btn) {
display: inline-block;
padding: 4px 14px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
color: #333;
text-align: center;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
vertical-align: middle;
cursor: pointer;
background-color: whiteSmoke;
background-image: -webkit-gradient(linear,0 0,0 100%,from(white),to(#E6E6E6));
background-image: -webkit-linear-gradient(top,white,#E6E6E6);
background-image: -o-linear-gradient(top,white,#E6E6E6);
background-image: linear-gradient(to bottom,white,#E6E6E6);
background-image: -moz-linear-gradient(top,white,#E6E6E6);
background-repeat: repeat-x;
border: 1px solid #BBB;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
border-bottom-color: #A2A2A2;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);
filter: progid:dximagetransform.microsoft.gradient(enabled=false);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);
-moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);
}
input[type='submit']:not(.btn):hover {
color: #333;
text-decoration: none;
background-color: #E6E6E6;
background-position: 0 -15px;
-webkit-transition: background-position .1s linear;
-moz-transition: background-position .1s linear;
-o-transition: background-position .1s linear;
transition: background-position .1s linear;
}
input[type='submit']:not(.btn).active, input[type='submit']:not(.btn):active {
background-color: #E6E6E6;
background-color: #D9D9D9 9;
background-image: none;
outline: 0;
-webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
-moz-box-shadow: inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
}
/*=============================================================
OTHER
==============================================================*/
.ie-lte8 .navbar-fixed-top {position:static;}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -21,8 +21,8 @@ jQuery(function(){
var title = escape(jQuery('title').text()); var title = escape(jQuery('title').text());
var twit = 'http://twitter.com/home?status='+title+'%20'+url; var twit = 'http://twitter.com/home?status='+title+'%20'+url;
var facebook = 'http://www.facebook.com/sharer.php?u='+url; var facebook = 'http://www.facebook.com/sharer.php?u='+url;
var buzz = 'http://www.google.com/reader/link?url='+url+'&amp;title='+title+'&amp;srcURL='+host; var gplus = 'https://plus.google.com/share?url='+url;
var tbar = '<div id="socialdrawer"><span>Share<br/></span><div id="sicons"><a href="'+twit+'" id="twit" title="Share on twitter"><img src="'+path+'/twitter.png" alt="Share on Twitter" width="32" height="32" /></a><a href="'+facebook+'" id="facebook" title="Share on Facebook"><img src="'+path+'/facebook.png" alt="Share on facebook" width="32" height="32" /></a><a href="'+buzz+'" id="buzz" title="Share on Buzz"><img src="'+path+'/google-buzz.png" alt="Share on Buzz" width="32" height="32" /></a></div></div>'; var tbar = '<div id="socialdrawer"><span>Share<br/></span><div id="sicons"><a href="'+twit+'" id="twit" title="Share on twitter"><img src="'+path+'/twitter.png" alt="Share on Twitter" width="32" height="32" /></a><a href="'+facebook+'" id="facebook" title="Share on Facebook"><img src="'+path+'/facebook.png" alt="Share on facebook" width="32" height="32" /></a><a href="'+gplus+'" id="gplus" title="Share on Google Plus"><img src="'+path+'/gplus-32.png" alt="Share on Google Plus" width="32" height="32" /></a></div></div>';
// Add the share tool bar. // Add the share tool bar.
jQuery('body').append(tbar); jQuery('body').append(tbar);
var st = jQuery('#socialdrawer'); var st = jQuery('#socialdrawer');
+48 -13
View File
@@ -55,7 +55,7 @@ function web2py_event_handlers() {
jQuery(function() { jQuery(function() {
var flash = jQuery('.flash'); var flash = jQuery('.flash');
flash.hide(); flash.hide();
if(flash.html()) flash.append('<span style="float:right;">&times;<span>').slideDown(); if(flash.html()) flash.append('<span style="float:right;">&times;</span>').slideDown();
web2py_ajax_init(document); web2py_ajax_init(document);
web2py_event_handlers(); web2py_event_handlers();
}); });
@@ -67,20 +67,20 @@ function web2py_trap_form(action,target) {
form.submit(function(e){ form.submit(function(e){
jQuery('.flash').hide().html(''); jQuery('.flash').hide().html('');
web2py_ajax_page('post',action,form.serialize(),target); web2py_ajax_page('post',action,form.serialize(),target);
e.preventDefault(); e.preventDefault();
}); });
}); });
} }
function web2py_trap_link(target) { function web2py_trap_link(target) {
jQuery('#'+target+' a.w2p_trap').each(function(i){ jQuery('#'+target+' a.w2p_trap').each(function(i){
var link=jQuery(this); var link=jQuery(this);
link.click(function(e) { link.click(function(e) {
jQuery('.flash').hide().html(''); jQuery('.flash').hide().html('');
web2py_ajax_page('get',link.attr('href'),[],target); web2py_ajax_page('get',link.attr('href'),[],target);
e.preventDefault(); e.preventDefault();
}); });
}); });
} }
function web2py_ajax_page(method, action, data, target) { function web2py_ajax_page(method, action, data, target) {
@@ -101,9 +101,9 @@ function web2py_ajax_page(method, action, data, target) {
web2py_trap_link(target); web2py_trap_link(target);
web2py_ajax_init('#'+target); web2py_ajax_init('#'+target);
if(command) if(command)
eval(decodeURIComponent(command)); eval(decodeURIComponent(command));
if(flash) if(flash)
jQuery('.flash').html(decodeURIComponent(flash)).slideDown(); jQuery('.flash').html(decodeURIComponent(flash)).slideDown();
} }
}); });
} }
@@ -151,11 +151,11 @@ function web2py_component(action, target, timeout, times){
} }
} else { } else {
// run once (no timeout specified) // run once (no timeout specified)
element.reload_counter = Infinity; element.reload_counter = Infinity;
web2py_ajax_page('get', action, null, target); web2py_ajax_page('get', action, null, target);
} }); } } }); }
function web2py_comet(url,onmessage,onopen,onclose) { function web2py_websocket(url,onmessage,onopen,onclose) {
if ("WebSocket" in window) { if ("WebSocket" in window) {
var ws = new WebSocket(url); var ws = new WebSocket(url);
ws.onopen = onopen?onopen:(function(){}); ws.onopen = onopen?onopen:(function(){});
@@ -165,3 +165,38 @@ function web2py_comet(url,onmessage,onopen,onclose) {
} else return false; // not supported } else return false; // not supported
} }
function web2py_calc_entropy(mystring) {
//calculate a simple entropy for a given string
var csets = new Array(
'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'0123456789', '!@#$\%^&*()', '~`-_=+[]{}\|;:\'",.<>?/',
'0123456789abcdefghijklmnopqrstuvwxyz');
var score = 0, other = {}, seen = {}, lastset = null, mystringlist = mystring.split('');
for (var i=0;i<mystringlist.length;i++) { // classify this character
var c = mystringlist[i], inset=5;
for(var j = 0; j<csets.length; j++)
if (csets[j].indexOf(c) != -1) {inset = j; break;}
//calculate effect of character on alphabet size
if(!(inset in seen)) {seen[inset] = 1;score += csets[inset].length;}
else if (!(c in other)) {score += 1;other[c] = 1;}
if (inset != lastset) {score += 1;lastset = inset;}
}
var entropy = mystring.length*Math.log(score)/0.6931471805599453;
return Math.round(entropy*100)/100
}
function web2py_validate_entropy(myfield, req_entropy) {
var validator = function () {
var v = (web2py_calc_entropy(myfield.val())||0)/req_entropy;
var r=0,g=0,b=0,rs=function(x){return Math.round(x*15).toString(16)};
if(v<=0.5) {r=1.0; g=2.0*v;}
else {r=(1.0-2.0*(Math.max(v,0)-0.5)); g=1.0;}
var color = '#'+rs(r)+rs(g)+rs(b);
myfield.css('background-color',color);
entropy_callback = myfield.data('entropy_callback');
if(entropy_callback) entropy_callback(v);
}
if(!myfield.hasClass('entropy_check')) myfield.on('keyup', validator).on('keydown', validator).addClass('entropy_check');
}
@@ -0,0 +1,28 @@
// this code improves bootstrap menus and adds dropdown support
jQuery(function(){
jQuery('.nav>li>a').each(function(){
if(jQuery(this).parent().find('ul').length)
jQuery(this).attr({'class':'dropdown-toggle','data-toggle':'dropdown'}).append('<b class="caret"></b>');
});
jQuery('.nav li li').each(function(){
if(jQuery(this).find('ul').length)
jQuery(this).addClass('dropdown-submenu');
});
function hoverMenu(){
var wid = document.documentElement.clientWidth; //faster than $(window).width() and cross browser
if (wid>=980){
jQuery('ul.nav a.dropdown-toggle').parent().hover(function(){
mi = jQuery(this).addClass('open');
mi.children('.dropdown-menu').stop(true, true).delay(200).fadeIn(400);
}, function(){
mi = jQuery(this);
mi.children('.dropdown-menu').stop(true, true).delay(200).fadeOut(function(){mi.removeClass('open')});
});
};
}
hoverMenu(); // first page load
jQuery(window).resize(hoverMenu); // on resize event
jQuery('ul.nav li.dropdown a').click(function(){window.location=jQuery(this).attr('href');});
// make all buttons bootstrap buttons
jQuery('button, form input[type="submit"], form input[type="button"]').addClass('btn');
});
+10 -4
View File
@@ -27,7 +27,7 @@
{{pass}} {{pass}}
{{else:}} {{else:}}
{{qry=''}} {{qry=''}}
{{pass}} {{pass}}
{{pass}} {{pass}}
<tr> <tr>
<th style="font-size: 1.75em;"> <th style="font-size: 1.75em;">
@@ -37,13 +37,19 @@
{{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn")}} {{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn")}}
</td> </td>
</tr> </tr>
{{pass}}
</table>
{{pass}} {{pass}}
{{pass}}
</table>
{{elif request.function=='select':}} {{elif request.function=='select':}}
<h2>{{=XML(str(T("Database %s select"))%A(request.args[0],_href=URL('index'))) }} <h2>{{=XML(str(T("Database %s select"))%A(request.args[0],_href=URL('index'))) }}
</h2> </h2>
{{if tb:}}
<h3>{{=T('Traceback')}}</h3>
<pre>
{{=tb}}
</pre>
{{pass}}
{{if table:}} {{if table:}}
{{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn")}}<br/><br/> {{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn")}}<br/><br/>
<h3>{{=T("Rows in Table")}}</h3><br/> <h3>{{=T("Rows in Table")}}</h3><br/>
@@ -149,7 +155,7 @@
<h4>{{=T("RAM")}}</h4> <h4>{{=T("RAM")}}</h4>
<p>{{=T.M("Number of entries: **%s**", ram['entries'])}}</p> <p>{{=T.M("Number of entries: **%s**", ram['entries'])}}</p>
{{if ram['entries'] > 0:}} {{if ram['entries'] > 0:}}
<p>{{=T.M("Hit Ratio: **%(ratio)s%%** (**%(hits)s** %{hit(hits)} and **%(misses)s** %%{miss(misses)})", <p>{{=T.M("Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})",
dict( ratio=ram['ratio'], hits=ram['hits'], misses=ram['misses']))}} dict( ratio=ram['ratio'], hits=ram['hits'], misses=ram['misses']))}}
</p> </p>
<p> <p>
+9 -5
View File
@@ -4,16 +4,20 @@
{{ {{
if request.args(0)=='login': if request.args(0)=='login':
if not 'register' in auth.settings.actions_disabled: if not 'register' in auth.settings.actions_disabled:
form.add_button(T('Register'),URL(args='register')) form.add_button(T('Register'),URL(args='register'),_class='btn')
pass pass
if not 'request_reset_password' in auth.settings.actions_disabled: if not 'request_reset_password' in auth.settings.actions_disabled:
form.add_button(T('Lost Password'),URL(args='request_reset_password')) form.add_button(T('Lost Password'),URL(args='request_reset_password'),_class='btn')
pass pass
pass pass
=form =form
}} }}
</div> </div>
<script language="javascript"><!-- <script language="javascript"><!--
jQuery("#web2py_user_form input:visible:enabled:first").focus(); jQuery("#web2py_user_form input:visible:enabled:first").focus();
//--></script> {{if request.args(0)=='register':}}
web2py_validate_entropy(jQuery('#auth_user_password'),100);
{{elif request.args(0)=='change_password':}}
web2py_validate_entropy(jQuery('#no_table_new_password'),100);
{{pass}}
//--></script>
+1 -1
View File
@@ -1 +1 @@
{{from gluon.serializers import xml}}{{=XML(xml(response._vars,quote=False))}} <?xml version="1.0" encoding="ISO-8859-1"?>{{from gluon.serializers import xml}}{{=XML(xml(response._vars,quote=False))}}
+7 -29
View File
@@ -39,10 +39,10 @@
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script> <script src="{{=URL('static','js/modernizr.custom.js')}}"></script>
<!-- include stylesheets --> <!-- include stylesheets -->
{{ {{
response.files.append(URL('static','css/web2py.css'))
response.files.append(URL('static','css/bootstrap.min.css')) response.files.append(URL('static','css/bootstrap.min.css'))
response.files.append(URL('static','css/bootstrap-responsive.min.css')) response.files.append(URL('static','css/bootstrap-responsive.min.css'))
response.files.append(URL('static','css/web2py.css'))
response.files.append(URL('static','css/web2py_bootstrap.css')) response.files.append(URL('static','css/web2py_bootstrap.css'))
}} }}
@@ -66,17 +66,17 @@
<body> <body>
<!-- Navbar ================================================== --> <!-- Navbar ================================================== -->
<div class="navbar navbar-fixed-top"> <div class="navbar navbar-inverse navbar-fixed-top">
<div class="flash">{{=response.flash or ''}}</div> <div class="flash">{{=response.flash or ''}}</div>
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container"> <div class="container">
<!-- the next tag is necessary for bootstrap menus, do not remove --> <!-- the next tag is necessary for bootstrap menus, do not remove -->
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</a> </button>
<a class="brand" href="#">web2py&trade;&nbsp;</a> <a class="brand" href="http://www.web2py.com/"><b>web<span>2</span>py</b>&trade;&nbsp;</a>
<ul id="navbar" class="nav pull-right">{{='auth' in globals() and auth.navbar(mode="dropdown") or ''}}</ul> <ul id="navbar" class="nav pull-right">{{='auth' in globals() and auth.navbar(mode="dropdown") or ''}}</ul>
<div class="nav-collapse"> <div class="nav-collapse">
{{is_mobile=request.user_agent().is_mobile}} {{is_mobile=request.user_agent().is_mobile}}
@@ -146,30 +146,8 @@
<!-- The javascript ============================================= <!-- The javascript =============================================
(Placed at the end of the document so the pages load faster) --> (Placed at the end of the document so the pages load faster) -->
<script>
// this code improves bootstrap menus and adds dropdown support
jQuery(function(){
jQuery('.nav>li>a').each(function(){
if(jQuery(this).parent().find('ul').length)
jQuery(this).attr({'class':'dropdown-toggle','data-toggle':'dropdown'}).append('<b class="caret"></b>');
});
jQuery('.nav li li').each(function(){
if(jQuery(this).find('ul').length)
jQuery(this).children('a').contents().before('<i class="chevron-right"></i>');
});
if(jQuery(document).width()>=980) {
jQuery('ul.nav li.dropdown').hover(function() {
jQuery(this).find('.dropdown-menu').stop(true, true).delay(200).fadeIn();
}, function() {
jQuery(this).find('.dropdown-menu').stop(true, true).delay(200).fadeOut();
});
}
jQuery('ul.nav li.dropdown a').click(function(){window.location=jQuery(this).attr('href');});
// make all buttons bootstrap buttons
jQuery('button, form input[type="submit"], form input[type="button"]').addClass('btn').css({'margin-right':'2px','margin-bottom':'2px'});
});
</script>
<script src="{{=URL('static','js/bootstrap.min.js')}}"></script> <script src="{{=URL('static','js/bootstrap.min.js')}}"></script>
<script src="{{=URL('static','js/web2py_bootstrap.js')}}"></script>
<!--[if lt IE 7 ]> <!--[if lt IE 7 ]>
<script src="{{=URL('static','js/dd_belatedpng.js')}}"></script> <script src="{{=URL('static','js/dd_belatedpng.js')}}"></script>
<script> DD_belatedPNG.fix('img, .png_bg'); //fix any <img> or .png_bg background-images </script> <script> DD_belatedPNG.fix('img, .png_bg'); //fix any <img> or .png_bg background-images </script>
-2
View File
@@ -83,9 +83,7 @@ def wsgiapp(env, res):
if global_settings.web2py_runtime == 'gae:development': if global_settings.web2py_runtime == 'gae:development':
gluon.admin.create_missing_folders() gluon.admin.create_missing_folders()
from gluon.custom_import import custom_import_install
web2py_path = global_settings.applications_parent # backward compatibility web2py_path = global_settings.applications_parent # backward compatibility
custom_import_install(web2py_path)
return gluon.main.wsgibase(env, res) return gluon.main.wsgibase(env, res)
+1 -1
View File
@@ -60,7 +60,7 @@ def app_pack(app, request, raise_ex=False):
""" """
try: try:
app_cleanup(app, request) app_cleanup(app, request)
filename = apath('../deposit/%s.w2p' % app, request) filename = apath('../deposit/web2py.app.%s.w2p' % app, request)
w2p_pack(filename, apath(app, request)) w2p_pack(filename, apath(app, request))
return filename return filename
except Exception, e: except Exception, e:
+105 -111
View File
@@ -11,7 +11,7 @@ Basic caching classes and methods
- Cache - The generic caching object interfacing with the others - Cache - The generic caching object interfacing with the others
- CacheInRam - providing caching in ram - CacheInRam - providing caching in ram
- CacheInDisk - provides caches on disk - CacheOnDisk - provides caches on disk
Memcache is also available via a different module (see gluon.contrib.memcache) Memcache is also available via a different module (see gluon.contrib.memcache)
@@ -46,6 +46,19 @@ class CacheAbstract(object):
Main function is now to provide referenced api documentation. Main function is now to provide referenced api documentation.
Use CacheInRam or CacheOnDisk instead which are derived from this class. Use CacheInRam or CacheOnDisk instead which are derived from this class.
Attentions, Michele says:
There are signatures inside gdbm files that are used directly
by the python gdbm adapter that often are lagging behind in the
detection code in python part.
On every occasion that a gdbm store is probed by the python adapter,
the probe fails, because gdbm file version is newer.
Using gdbm directly from C would work, because there is backward
compatibility, but not from python!
The .shelve file is discarded and a new one created (with new
signature) and it works until it is probed again...
The possible consequences are memory leaks and broken sessions.
""" """
cache_stats_name = 'web2py_cache_statistics' cache_stats_name = 'web2py_cache_statistics'
@@ -130,22 +143,27 @@ class CacheInRam(CacheAbstract):
meta_storage = {} meta_storage = {}
def __init__(self, request=None): def __init__(self, request=None):
self.locker.acquire() self.initialized = False
self.request = request self.request = request
def initialize(self):
if self.initialized: return
else: self.initialized = True
self.locker.acquire()
request = self.request
if request: if request:
app = request.application app = request.application
else: else:
app = '' app = ''
if not app in self.meta_storage: if not app in self.meta_storage:
self.storage = self.meta_storage[app] = {CacheAbstract.cache_stats_name: { self.storage = self.meta_storage[app] = {
'hit_total': 0, CacheAbstract.cache_stats_name: {'hit_total': 0, 'misses': 0}}
'misses': 0,
}}
else: else:
self.storage = self.meta_storage[app] self.storage = self.meta_storage[app]
self.locker.release() self.locker.release()
def clear(self, regex=None): def clear(self, regex=None):
self.initialize()
self.locker.acquire() self.locker.acquire()
storage = self.storage storage = self.storage
if regex is None: if regex is None:
@@ -154,10 +172,7 @@ class CacheInRam(CacheAbstract):
self._clear(storage, regex) self._clear(storage, regex)
if not CacheAbstract.cache_stats_name in storage.keys(): if not CacheAbstract.cache_stats_name in storage.keys():
storage[CacheAbstract.cache_stats_name] = { storage[CacheAbstract.cache_stats_name] = {'hit_total': 0,'misses': 0}
'hit_total': 0,
'misses': 0,
}
self.locker.release() self.locker.release()
@@ -172,6 +187,7 @@ class CacheInRam(CacheAbstract):
3) would work unless we deepcopy no storage and retrival which would make things slow. 3) would work unless we deepcopy no storage and retrival which would make things slow.
Anyway. You can deepcopy explicitly in the function generating the value to be cached. Anyway. You can deepcopy explicitly in the function generating the value to be cached.
""" """
self.initialize()
dt = time_expire dt = time_expire
now = time.time() now = time.time()
@@ -200,6 +216,7 @@ class CacheInRam(CacheAbstract):
return value return value
def increment(self, key, value=1): def increment(self, key, value=1):
self.initialize()
self.locker.acquire() self.locker.acquire()
try: try:
if key in self.storage: if key in self.storage:
@@ -226,58 +243,65 @@ class CacheOnDisk(CacheAbstract):
Values stored in disk cache must be pickable. Values stored in disk cache must be pickable.
""" """
speedup_checks = set() def _close_shelve_and_unlock(self):
try:
if self.storage:
self.storage.close()
finally:
if self.locker and self.locked:
portalocker.unlock(self.locker)
self.locker.close()
self.locked = False
def _open_shelf_with_lock(self): def _open_shelve_and_lock(self):
"""Open and return a shelf object, obtaining an exclusive lock """Open and return a shelf object, obtaining an exclusive lock
on self.locker first. Replaces the close method of the on self.locker first. Replaces the close method of the
returned shelf instance with one that releases the lock upon returned shelf instance with one that releases the lock upon
closing.""" closing."""
def _close(self):
try: storage = None
shelve.Shelf.close(self) locker = None
finally: locked = False
portalocker.unlock(self.locker)
self.locker.close()
storage, locker, locker_locked = None, None, False
try: try:
locker = open(self.locker_name, 'a') locker = locker = open(self.locker_name, 'a')
portalocker.lock(locker, portalocker.LOCK_EX) portalocker.lock(locker, portalocker.LOCK_EX)
locker_locked = True locked = True
storage = shelve.open(self.shelve_name) try:
storage.close = _close.__get__(storage, shelve.Shelf) storage = shelve.open(self.shelve_name)
storage.locker = locker except:
except Exception: logger.error('corrupted cache file %s, will try rebuild it' \
logger.error('corrupted cache file %s, will try to delete and recreate it!' % (self.shelve_name)) % (self.shelve_name))
storage = None
if not storage and os.path.exists(self.shelve_name):
os.unlink(self.shelve_name)
storage = shelve.open(self.shelve_name)
if not CacheAbstract.cache_stats_name in storage.keys():
storage[CacheAbstract.cache_stats_name] = {'hit_total':0, 'misses': 0}
storage.sync()
except Exception, e:
if storage: if storage:
storage.close() storage.close()
storage = None storage = None
if locked:
try: portalocker.unlock(locker)
os.unlink(self.shelve_name) locker.close()
storage = shelve.open(self.shelve_name) locked = False
storage.close = _close.__get__(storage, shelve.Shelf) raise RuntimeError, 'unable to create/re-create cache file %s' % self.shelve_name
storage.locker = locker self.locker = locker
if not CacheAbstract.cache_stats_name in storage.keys(): self.locked = locked
storage[CacheAbstract.cache_stats_name] = { self.storage = storage
'hit_total': 0,
'misses': 0,
}
storage.sync()
except (IOError, OSError):
logger.warn('unable to delete and recreate cache file %s' % self.shelve_name)
if storage:
storage.close()
storage = None
if locker_locked:
portalocker.unlock(locker)
if locker:
locker.close()
return storage return storage
def __init__(self, request, folder=None): def __init__(self, request=None, folder=None):
self.initialized = False
self.request = request self.request = request
self.folder = folder
def initialize(self):
if self.initialized: return
else: self.initialized = True
folder = self.folder
request = self.request
# Lets test if the cache folder exists, if not # Lets test if the cache folder exists, if not
# we are going to create it # we are going to create it
@@ -291,93 +315,54 @@ class CacheOnDisk(CacheAbstract):
self.locker_name = os.path.join(folder,'cache.lock') self.locker_name = os.path.join(folder,'cache.lock')
self.shelve_name = os.path.join(folder,'cache.shelve') self.shelve_name = os.path.join(folder,'cache.shelve')
speedup_key = (folder,CacheAbstract.cache_stats_name)
if not speedup_key in self.speedup_checks or \
not os.path.exists(self.shelve_name):
try:
storage = self._open_shelf_with_lock()
try:
if not CacheAbstract.cache_stats_name in storage:
storage[CacheAbstract.cache_stats_name] = {
'hit_total': 0,
'misses': 0,
}
storage.sync()
finally:
storage.close()
self.speedup_checks.add(speedup_key)
except ImportError:
pass # no module _bsddb, ignoring exception now so it makes a ticket only if used
def clear(self, regex=None): def clear(self, regex=None):
storage = self._open_shelf_with_lock() self.initialize()
storage = self._open_shelve_and_lock()
try: try:
if regex is None: if regex is None:
storage.clear() storage.clear()
else: else:
self._clear(storage, regex) self._clear(storage, regex)
if not CacheAbstract.cache_stats_name in storage.keys():
storage[CacheAbstract.cache_stats_name] = {
'hit_total': 0,
'misses': 0,
}
storage.sync() storage.sync()
finally: finally:
storage.close() self._close_shelve_and_unlock()
def __call__(self, key, f, def __call__(self, key, f,
time_expire = DEFAULT_TIME_EXPIRE): time_expire = DEFAULT_TIME_EXPIRE):
self.initialize()
dt = time_expire dt = time_expire
storage = self._open_shelve_and_lock()
storage = self._open_shelf_with_lock()
try: try:
item = storage.get(key, None) item = storage.get(key, None)
storage[CacheAbstract.cache_stats_name]['hit_total'] += 1
if item and f is None: if item and f is None:
del storage[key] del storage[key]
storage.sync()
storage[CacheAbstract.cache_stats_name] = { now = time.time()
'hit_total': storage[CacheAbstract.cache_stats_name]['hit_total'] + 1, if f is None:
'misses': storage[CacheAbstract.cache_stats_name]['misses'] value = None
} elif item and (dt is None or item[0] > now - dt):
value = item[1]
storage.sync() else:
value = f()
storage[key] = (now, value)
storage[CacheAbstract.cache_stats_name]['misses']+=1
storage.sync()
finally: finally:
if storage: self._close_shelve_and_unlock()
storage.close()
now = time.time()
if f is None:
return None
if item and (dt is None or item[0] > now - dt):
return item[1]
value = f()
storage = self._open_shelf_with_lock()
try:
storage[key] = (now, value)
storage[CacheAbstract.cache_stats_name] = {
'hit_total': storage[CacheAbstract.cache_stats_name]['hit_total'],
'misses': storage[CacheAbstract.cache_stats_name]['misses'] + 1
}
storage.sync()
finally:
if storage:
storage.close()
return value return value
def increment(self, key, value=1): def increment(self, key, value=1):
storage = self._open_shelf_with_lock() self.initialize()
storage = self._open_shelve_and_lock()
try: try:
if key in storage: if key in storage:
value = storage[key][1] + value value = storage[key][1] + value
storage[key] = (time.time(), value) storage[key] = (time.time(), value)
storage.sync() storage.sync()
finally: finally:
if storage: self._close_shelve_and_unlock()
storage.close()
return value return value
class CacheAction(object): class CacheAction(object):
@@ -423,10 +408,9 @@ class Cache(object):
the global request object the global request object
""" """
# GAE will have a special caching # GAE will have a special caching
if have_settings and settings.global_settings.web2py_runtime_gae: if have_settings and settings.global_settings.web2py_runtime_gae:
from contrib.gae_memcache import MemcacheClient from contrib.gae_memcache import MemcacheClient
self.ram=self.disk=MemcacheClient(request) self.ram = self.disk = MemcacheClient(request)
else: else:
# Otherwise use ram (and try also disk) # Otherwise use ram (and try also disk)
self.ram = CacheInRam(request) self.ram = CacheInRam(request)
@@ -479,6 +463,16 @@ class Cache(object):
return CacheAction(func,key,time_expire,self,cache_model) return CacheAction(func,key,time_expire,self,cache_model)
return tmp return tmp
@staticmethod
def with_prefix(cache_model, prefix):
"""
allow replacing cache.ram with cache.with_prefix(cache.ram,'prefix')
it will add prefix to all the cache keys used.
"""
return lambda key, f, time_expire=DEFAULT_TIME_EXPIRE, prefix=prefix:\
cache_model(prefix + key, f, time_expire)
def lazy_cache(key=None,time_expire=None,cache_model='ram'): def lazy_cache(key=None,time_expire=None,cache_model='ram'):
""" """
can be used to cache any function including in modules, can be used to cache any function including in modules,
+29 -27
View File
@@ -40,6 +40,7 @@ import imp
import logging import logging
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
import rewrite import rewrite
from custom_import import custom_import_install
try: try:
import py_compile import py_compile
@@ -358,22 +359,36 @@ OLD IMPLEMENTATION:
return module return module
""" """
_base_environment_ = dict((k,getattr(html,k)) for k in html.__all__)
_base_environment_.update((k,getattr(validators,k)) for k in validators.__all__)
_base_environment_['__builtins__'] = __builtins__
_base_environment_['HTTP'] = HTTP
_base_environment_['redirect'] = redirect
_base_environment_['DAL'] = DAL
_base_environment_['Field'] = Field
_base_environment_['SQLDB'] = SQLDB # for backward compatibility
_base_environment_['SQLField'] = SQLField # for backward compatibility
_base_environment_['SQLFORM'] = SQLFORM
_base_environment_['SQLTABLE'] = SQLTABLE
_base_environment_['LOAD'] = LOAD
def build_environment(request, response, session, store_current=True): def build_environment(request, response, session, store_current=True):
""" """
Build the environment dictionary into which web2py files are executed. Build the environment dictionary into which web2py files are executed.
""" """
h,v = html,validators #h,v = html,validators
environment = dict((k,getattr(h,k)) for k in h.__all__) environment = dict(_base_environment_)
environment.update((k,getattr(v, k)) for k in v.__all__)
if not request.env: if not request.env:
request.env = Storage() request.env = Storage()
# Enable standard conditional models (i.e., /*.py, /[controller]/*.py, and # Enable standard conditional models (i.e., /*.py, /[controller]/*.py, and
# /[controller]/[function]/*.py) # /[controller]/[function]/*.py)
response.models_to_run = [r'^\w+\.py$', r'^%s/\w+\.py$' % request.controller, response.models_to_run = [r'^\w+\.py$', r'^%s/\w+\.py$' % request.controller,
r'^%s/%s/\w+\.py$' % (request.controller, request.function)] r'^%s/%s/\w+\.py$' % (request.controller, request.function)]
t = environment['T'] = translator(request) t = environment['T'] = translator(request)
c = environment['cache'] = Cache(request) c = environment['cache'] = Cache(request)
if store_current: if store_current:
current.globalenv = environment current.globalenv = environment
current.request = request current.request = request
@@ -389,27 +404,17 @@ def build_environment(request, response, session, store_current=True):
__builtins__ = mybuiltin() __builtins__ = mybuiltin()
else: else:
__builtins__['__import__'] = __builtin__.__import__ ### WHY? __builtins__['__import__'] = __builtin__.__import__ ### WHY?
environment['__builtins__'] = __builtins__
environment['HTTP'] = HTTP
environment['redirect'] = redirect
environment['request'] = request environment['request'] = request
environment['response'] = response environment['response'] = response
environment['session'] = session environment['session'] = session
environment['DAL'] = DAL
environment['Field'] = Field
environment['SQLDB'] = SQLDB # for backward compatibility
environment['SQLField'] = SQLField # for backward compatibility
environment['SQLFORM'] = SQLFORM
environment['SQLTABLE'] = SQLTABLE
environment['LOAD'] = LOAD
environment['local_import'] = \ environment['local_import'] = \
lambda name, reload=False, app=request.application:\ lambda name, reload=False, app=request.application:\
local_import_aux(name,reload,app) local_import_aux(name,reload,app)
BaseAdapter.set_folder(pjoin(request.folder, 'databases')) BaseAdapter.set_folder(pjoin(request.folder, 'databases'))
response._view_environment = copy.copy(environment) response._view_environment = copy.copy(environment)
custom_import_install()
return environment return environment
def save_pyc(filename): def save_pyc(filename):
""" """
Bytecode compiles the file `filename` Bytecode compiles the file `filename`
@@ -527,7 +532,6 @@ def run_controller_in(controller, function, environment):
""" """
# if compiled should run compiled! # if compiled should run compiled!
folder = environment['request'].folder folder = environment['request'].folder
path = pjoin(folder, 'compiled') path = pjoin(folder, 'compiled')
badc = 'invalid controller (%s/%s)' % (controller, function) badc = 'invalid controller (%s/%s)' % (controller, function)
@@ -537,7 +541,7 @@ def run_controller_in(controller, function, environment):
% (controller, function)) % (controller, function))
if not os.path.exists(filename): if not os.path.exists(filename):
raise HTTP(404, raise HTTP(404,
rewrite.thread.routes.error_message % badf, rewrite.THREAD_LOCAL.routes.error_message % badf,
web2py_error=badf) web2py_error=badf)
restricted(read_pyc(filename), environment, layer=filename) restricted(read_pyc(filename), environment, layer=filename)
elif function == '_TEST': elif function == '_TEST':
@@ -552,7 +556,7 @@ def run_controller_in(controller, function, environment):
% controller) % controller)
if not os.path.exists(filename): if not os.path.exists(filename):
raise HTTP(404, raise HTTP(404,
rewrite.thread.routes.error_message % badc, rewrite.THREAD_LOCAL.routes.error_message % badc,
web2py_error=badc) web2py_error=badc)
environment['__symbols__'] = environment.keys() environment['__symbols__'] = environment.keys()
code = read_file(filename) code = read_file(filename)
@@ -563,13 +567,13 @@ def run_controller_in(controller, function, environment):
% controller) % controller)
if not os.path.exists(filename): if not os.path.exists(filename):
raise HTTP(404, raise HTTP(404,
rewrite.thread.routes.error_message % badc, rewrite.THREAD_LOCAL.routes.error_message % badc,
web2py_error=badc) web2py_error=badc)
code = read_file(filename) code = read_file(filename)
exposed = regex_expose.findall(code) exposed = regex_expose.findall(code)
if not function in exposed: if not function in exposed:
raise HTTP(404, raise HTTP(404,
rewrite.thread.routes.error_message % badf, rewrite.THREAD_LOCAL.routes.error_message % badf,
web2py_error=badf) web2py_error=badf)
code = "%s\nresponse._vars=response._caller(%s)\n" % (code, function) code = "%s\nresponse._vars=response._caller(%s)\n" % (code, function)
if is_gae: if is_gae:
@@ -579,8 +583,7 @@ def run_controller_in(controller, function, environment):
response = environment['response'] response = environment['response']
vars=response._vars vars=response._vars
if response.postprocessing: if response.postprocessing:
for p in response.postprocessing: vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars)
vars = p(vars)
if isinstance(vars,unicode): if isinstance(vars,unicode):
vars = vars.encode('utf8') vars = vars.encode('utf8')
elif hasattr(vars,'xml') and callable(vars.xml): elif hasattr(vars,'xml') and callable(vars.xml):
@@ -594,7 +597,6 @@ def run_view_in(environment):
or `view/generic.extension` or `view/generic.extension`
It tries the pre-compiled views_controller_function.pyc before compiling it. It tries the pre-compiled views_controller_function.pyc before compiling it.
""" """
request = environment['request'] request = environment['request']
response = environment['response'] response = environment['response']
view = response.view view = response.view
@@ -630,7 +632,7 @@ def run_view_in(environment):
restricted(code, environment, layer=filename) restricted(code, environment, layer=filename)
return return
raise HTTP(404, raise HTTP(404,
rewrite.thread.routes.error_message % badv, rewrite.THREAD_LOCAL.routes.error_message % badv,
web2py_error=badv) web2py_error=badv)
else: else:
filename = pjoin(folder, 'views', view) filename = pjoin(folder, 'views', view)
@@ -639,7 +641,7 @@ def run_view_in(environment):
filename = pjoin(folder, 'views', view) filename = pjoin(folder, 'views', view)
if not os.path.exists(filename): if not os.path.exists(filename):
raise HTTP(404, raise HTTP(404,
rewrite.thread.routes.error_message % badv, rewrite.THREAD_LOCAL.routes.error_message % badv,
web2py_error=badv) web2py_error=badv)
layer = filename layer = filename
if is_gae: if is_gae:
+22 -13
View File
@@ -12,47 +12,56 @@ cache.ram=cache.disk=MemcacheClient(request)
import time import time
from google.appengine.api.memcache import Client from google.appengine.api.memcache import Client
class MemcacheClient(object):
class MemcacheClient(Client): client = Client()
def __init__(self, request): def __init__(self, request):
self.request = request self.request = request
Client.__init__(self)
def __call__( def __call__(
self, self,
key, key,
f, f,
time_expire=300, time_expire=300,
): ):
key = '%s/%s' % (self.request.application, key) key = '%s/%s' % (self.request.application, key)
dt = time_expire dt = time_expire
value = None value = None
obj = self.get(key) obj = self.client.get(key)
if obj and (dt == None or obj[0] > time.time() - dt): if obj and (dt == None or obj[0] > time.time() - dt):
value = obj[1] value = obj[1]
elif f is None: elif f is None:
if obj: if obj:
self.delete(key) self.client.delete(key)
else: else:
value = f() value = f()
self.set(key, (time.time(), value)) self.client.set(key, (time.time(), value))
return value return value
def increment(self, key, value=1): def increment(self, key, value=1):
key = '%s/%s' % (self.request.application, key) key = '%s/%s' % (self.request.application, key)
obj = self.get(key) obj = self.client.get(key)
if obj: if obj:
value = obj[1] + value value = obj[1] + value
self.set((time.time(), value)) self.client.set(key, (time.time(), value))
return value return value
def clear(self, key): def clear(self, key = None):
key = '%s/%s' % (self.request.application, key) if key:
self.delete(key) key = '%s/%s' % (self.request.application, key)
self.client.delete(key)
else:
self.client.flush_all()
def delete(self,*a,**b):
return self.client.delete(*a,**b)
def get(self,*a,**b):
return self.client.delete(*a,**b)
def set(self,*a,**b):
return self.client.delete(*a,**b)
def flush_all(self,*a,**b):
return self.client.delete(*a,**b)
+36 -27
View File
@@ -2,9 +2,9 @@
# coding: utf8 # coding: utf8
""" """
Dropbox Authentication for web2py Dropbox Authentication for web2py
Developed by Massimo Di Pierro (2012) Developed by Massimo Di Pierro (2012)
Same License as Web2py License Same License as Web2py License
""" """
# mind here session is dropbox session, not current.session # mind here session is dropbox session, not current.session
@@ -27,7 +27,7 @@ class DropboxAccount(object):
key="...", key="...",
secret="...", secret="...",
access_type="...", access_type="...",
url = "http://localhost:8000/%s/default/user/login" % request.application) login_url = "http://localhost:8000/%s/default/user/login" % request.application)
when logged in when logged in
client = auth.settings.login_form.client client = auth.settings.login_form.client
""" """
@@ -40,7 +40,7 @@ class DropboxAccount(object):
login_url = "", login_url = "",
on_login_failure=None, on_login_failure=None,
): ):
self.request=request self.request=request
self.key=key self.key=key
self.secret=secret self.secret=secret
@@ -53,39 +53,48 @@ class DropboxAccount(object):
def get_user(self): def get_user(self):
request = self.request request = self.request
token = current.session.dropbox_token if not current.session.dropbox_request_token:
if not token:
return None
try:
self.sess.set_token(token[0],token[1])
except:
# invalid token, should never happen
return None return None
elif not current.session.dropbox_access_token:
request_token = current.session.dropbox_request_token
self.sess.set_request_token(request_token[0],request_token[1])
access_token = self.sess.obtain_access_token(self.sess.token)
current.session.dropbox_access_token = \
(access_token.key,access_token.secret)
else: else:
user = Storage() access_token = current.session.dropbox_access_token
self.client = client.DropboxClient(self.sess) self.sess.set_token(access_token[0],access_token[1])
data = self.client.account_info()
display_name = data.get('display_name','').split(' ',1)
user = dict(email = data.get('email',None), user = Storage()
first_name = display_name[0], self.client = client.DropboxClient(self.sess)
last_name = display_name[-1], data = self.client.account_info()
registration_id = data.get('uid',None)) display_name = data.get('display_name','').split(' ',1)
if not user['registration_id'] and self.on_login_failure: user = dict(email = data.get('email',None),
redirect(self.on_login_failure) first_name = display_name[0],
return user last_name = display_name[-1],
registration_id = data.get('uid',None))
if not user['registration_id'] and self.on_login_failure:
redirect(self.on_login_failure)
return user
def login_form(self): def login_form(self):
token = self.sess.obtain_request_token()
current.session.dropbox_token = (token.key,token.secret) request_token = self.sess.obtain_request_token()
dropbox_url = self.sess.build_authorize_url(token,self.login_url) current.session.dropbox_request_token = \
(request_token.key,request_token.secret)
dropbox_url = self.sess.build_authorize_url(request_token,
self.login_url)
redirect(dropbox_url) redirect(dropbox_url)
form = IFRAME(_src=dropbox_url, form = IFRAME(_src=dropbox_url,
_scrolling="no", _scrolling="no",
_frameborder="no", _frameborder="no",
_style="width:400px;height:240px;") _style="width:400px;height:240px;")
return form return form
def logout_url(self, next = "/"): def logout_url(self, next = "/"):
current.session.dropbox_token=None current.session.dropbox_request_token=None
current.session.auth=None current.session.auth=None
redirect('https://www.dropbox.com/logout') redirect('https://www.dropbox.com/logout')
return next return next
@@ -107,7 +107,7 @@ server for requests. It can be used for the optional"scope" parameters for Face
path_info = next path_info = next
else: else:
path_info = r.env.path_info path_info = r.env.path_info
uri = '%s://%s%s' %(url_scheme, http_host, path_info) uri = '%s://%s%s' % (url_scheme, http_host, path_info)
if r.get_vars and not next: if r.get_vars and not next:
uri += '?' + urlencode(r.get_vars) uri += '?' + urlencode(r.get_vars)
return uri return uri
+12 -5
View File
@@ -26,8 +26,10 @@ class MemcacheClientObj(Client):
def __init__(self, request, servers, debug=0, pickleProtocol=0, def __init__(self, request, servers, debug=0, pickleProtocol=0,
pickler=pickle.Pickler, unpickler=pickle.Unpickler, pickler=pickle.Pickler, unpickler=pickle.Unpickler,
pload=None, pid=None): pload=None, pid=None,
default_time_expire = DEFAULT_TIME_EXPIRE):
self.request=request self.request=request
self.default_time_expire = default_time_expire
if request: if request:
app = request.application app = request.application
else: else:
@@ -43,8 +45,9 @@ class MemcacheClientObj(Client):
else: else:
self.storage = self.meta_storage[app] self.storage = self.meta_storage[app]
def __call__(self, key, f, def __call__(self, key, f, time_expire = 'default'):
time_expire=DEFAULT_TIME_EXPIRE): if time_expire == 'default':
time_expire = self.default_time_expire
if time_expire == None: if time_expire == None:
time_expire = self.max_time_expire time_expire = self.max_time_expire
# this must be commented because get and set are redefined # this must be commented because get and set are redefined
@@ -70,8 +73,10 @@ class MemcacheClientObj(Client):
self.set(key, (now,value), self.max_time_expire) self.set(key, (now,value), self.max_time_expire)
return value return value
def increment(self, key, value=1, time_expire=DEFAULT_TIME_EXPIRE): def increment(self, key, value=1, time_expire='default'):
""" time_expire is ignored """ """ time_expire is ignored """
if time_expire == 'default':
time_expire = self.default_time_expire
newKey = self.__keyFormat__(key) newKey = self.__keyFormat__(key)
obj = Client.get(self, newKey) obj = Client.get(self, newKey)
if obj: if obj:
@@ -86,7 +91,9 @@ class MemcacheClientObj(Client):
Client.set(self, newKey, value, self.max_time_expire) Client.set(self, newKey, value, self.max_time_expire)
return value return value
def set(self, key, value, time_expire=DEFAULT_TIME_EXPIRE): def set(self, key, value, time_expire='default'):
if time_expire == 'default':
time_expire = self.default_time_expire
newKey = self.__keyFormat__(key) newKey = self.__keyFormat__(key)
return Client.set(self, newKey, value, time_expire) return Client.set(self, newKey, value, time_expire)
+35 -15
View File
@@ -12,6 +12,7 @@ import cssmin
import jsmin import jsmin
import os import os
import hashlib import hashlib
import re
def read_binary_file(filename): def read_binary_file(filename):
f = open(filename,'rb') f = open(filename,'rb')
@@ -24,9 +25,10 @@ def write_binary_file(filename,data):
f.write(data) f.write(data)
f.close() f.close()
def fix_links(css,static_path): def fix_links(css,static_path):
return css.replace('../',static_path+'/') return re.sub(r'url\((["\'])\.\./', 'url(\\1' + static_path, css)
def minify(files, path_info, folder, optimize_css, optimize_js, def minify(files, path_info, folder, optimize_css, optimize_js,
ignore_concat = [], ignore_concat = [],
ignore_minify = ['/jquery.js', '/anytime.js']): ignore_minify = ['/jquery.js', '/anytime.js']):
@@ -60,17 +62,30 @@ def minify(files, path_info, folder, optimize_css, optimize_js,
processed = [] processed = []
for k,filename in enumerate(files): for k,filename in enumerate(files):
if not filename.startswith('/') or \ if not filename.startswith('/') or \
any(filename.endswith(x) for x in ignore_concat): any(filename.endswith(x) \
for x in ignore_concat):
new_files.append(filename) new_files.append(filename)
continue continue
abs_filename = os.path.join(folder,'static', abs_filename = os.path.join(
filename[len(static_path)+1:]) folder,'static', filename[len(static_path)+1:])
if filename.lower().endswith('.css'): if filename.lower().endswith('.css'):
processed.append(filename) processed.append(filename)
spath_info, sfilename = \
path_info.split('/'), filename.split('/')
u = 0
for i,a in enumerate(sfilename):
try:
if a != spath_info[i]:
u = i
break
except:
pass
if concat_css: if concat_css:
contents = read_binary_file(abs_filename) contents = read_binary_file(abs_filename)
replacement = '/'.join(spath_info[:u]) + '/'
contents = fix_links(contents, replacement)
if minify_css: if minify_css:
css.append(cssmin.cssmin(contents)) css.append(cssmin.cssmin(contents))
else: else:
@@ -80,9 +95,12 @@ def minify(files, path_info, folder, optimize_css, optimize_js,
elif filename.lower().endswith('.js'): elif filename.lower().endswith('.js'):
processed.append(filename) processed.append(filename)
if concat_js: if concat_js:
contents = read_binary_file(abs_filename) contents = read_binary_file(abs_filename)
if minify_js and not filename.endswith('.min.js') and \
not any(filename.endswith(x) for x in ignore_minify): if minify_js and \
not filename.endswith('.min.js') and \
not any(filename.endswith(x) \
for x in ignore_minify):
js.append(jsmin.jsmin(contents)) js.append(jsmin.jsmin(contents))
else: else:
js.append(contents) js.append(contents)
@@ -91,16 +109,17 @@ def minify(files, path_info, folder, optimize_css, optimize_js,
dest_key = hashlib.md5(repr(processed)).hexdigest() dest_key = hashlib.md5(repr(processed)).hexdigest()
if css and concat_css: if css and concat_css:
css = '\n\n'.join(contents for contents in css) css = '\n\n'.join(contents for contents in css)
if inline_css: if not inline_css:
css = ('css:inline',fix_links(css,static_path))
else:
temppath = os.path.join(folder,'static',temp) temppath = os.path.join(folder,'static',temp)
if not os.path.exists(temppath): os.mkdir(temppath) if not os.path.exists(temppath):
os.mkdir(temppath)
dest = "compressed_%s.css" % dest_key dest = "compressed_%s.css" % dest_key
tempfile = os.path.join(temppath, dest) tempfile = os.path.join(temppath, dest)
write_binary_file(tempfile,css) write_binary_file(tempfile,css)
css = path_info+'/%s' % dest css = path_info+'/%s' % dest
new_files.append(css) new_files.append(css)
else:
new_files.append(('css:inline',css))
else: else:
new_files += css new_files += css
if js and concat_js: if js and concat_js:
@@ -109,7 +128,8 @@ def minify(files, path_info, folder, optimize_css, optimize_js,
js = ('js:inline',js) js = ('js:inline',js)
else: else:
temppath = os.path.join(folder,'static',temp) temppath = os.path.join(folder,'static',temp)
if not os.path.exists(temppath): os.mkdir(temppath) if not os.path.exists(temppath):
os.mkdir(temppath)
dest = "compressed_%s.js" % dest_key dest = "compressed_%s.js" % dest_key
tempfile = os.path.join(folder,'static',temp,dest) tempfile = os.path.join(folder,'static',temp,dest)
write_binary_file(tempfile,js) write_binary_file(tempfile,js)
+19
View File
@@ -0,0 +1,19 @@
# Copyright (c) 2006-2012 James Tauber and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
# pyuca: Python Unicode Collation Algorithm implementation
(http://jtauber.com/blog/2006/01/27/python_unicode_collation_algorithm/)
This is my preliminary attempt at a Python implementation of the
[Unicode Collation Algorithm (UCA)](http://unicode.org/reports/tr10/).
I originally posted it to my blog in 2006 but it seems to get enough
usage it really belongs here (and in PyPI).
What do you use it for? In short, sorting non-English strings properly.
The core of the algorithm involves multi-level comparison. For example,
``café`` comes before ``caff`` because at the primary level, the accent
is ignored and the first word is treated as if it were ``cafe``.
The secondary level (which considers accents) only applies then to words
that are equivalent at the primary level.
The Unicode Collation Algorithm and pyuca also support contraction and
expansion. **Contraction** is where multiple letters are treated as a
single unit. In Spanish, ``ch`` is treated as a letter coming between
``c`` and ``d`` so that, for example, words beginning ``ch`` should
sort after all other words beginnings with ``c``. **Expansion** is where
a single letter is treated as though it were multiple letters. In German,
``ä`` is sorted as if it were ``ae``, i.e. after ``ad`` but before ``af``.
## Here is how to use the ``pyuca`` module:
``
git clone https://github.com/jtauber/pyuca.git
cd pyuca
pip install pyuca
``
**Usage example:**
``
from pyuca import Collator
c = Collator("allkeys.txt")
sorted_words = sorted(words, key=c.sort_key)
``
``allkeys.txt`` (1 MB) is available at
http://www.unicode.org/Public/UCA/latest/allkeys.txt
+6 -1
View File
@@ -1,5 +1,10 @@
import os import os
import pyuca import pyuca
unicode_collator = pyuca.Collator(os.path.join(os.path.dirname(__file__), 'allkeys.txt')) unicode_collator = None
def set_unicode_collator(file):
global unicode_collator
unicode_collator = pyuca.Collator(file)
set_unicode_collator(os.path.join(os.path.dirname(__file__), 'allkeys.txt'))
+38 -27
View File
@@ -1,21 +1,21 @@
# pyuca - Unicode Collation Algorithm # pyuca - Unicode Collation Algorithm
# Version: 2006-02-13 # Version: 2012-06-21
# #
# James Tauber # James Tauber
# http://jtauber.com/ # http://jtauber.com/
# Copyright (c) 2006 James Tauber # Copyright (c) 2006-2012 James Tauber and contributors
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights # in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is # copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions: # furnished to do so, subject to the following conditions:
# #
# The above copyright notice and this permission notice shall be included in # The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software. # all copies or substantial portions of the Software.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -28,7 +28,6 @@
""" """
Preliminary implementation of the Unicode Collation Algorithm. Preliminary implementation of the Unicode Collation Algorithm.
This only implements the simple parts of the algorithm but I have successfully This only implements the simple parts of the algorithm but I have successfully
tested it using the Default Unicode Collation Element Table (DUCET) to collate tested it using the Default Unicode Collation Element Table (DUCET) to collate
Ancient Greek correctly. Ancient Greek correctly.
@@ -48,31 +47,39 @@ but you can always subset this for just the characters you are dealing with.
""" """
class Trie: class Node:
def __init__(self): def __init__(self):
self.root = [None, {}] self.value = None
self.children = {}
class Trie:
def __init__(self):
self.root = Node()
def add(self, key, value): def add(self, key, value):
curr_node = self.root curr_node = self.root
for part in key: for part in key:
curr_node = curr_node[1].setdefault(part, [None, {}]) curr_node = curr_node.children.setdefault(part, Node())
curr_node[0] = value curr_node.value = value
def find_prefix(self, key): def find_prefix(self, key):
curr_node = self.root curr_node = self.root
remainder = key remainder = key
for part in key: for part in key:
if part not in curr_node[1]: if part not in curr_node.children:
break break
curr_node = curr_node[1][part] curr_node = curr_node.children[part]
remainder = remainder[1:] remainder = remainder[1:]
return (curr_node[0], remainder) return (curr_node.value, remainder)
class Collator: class Collator:
def __init__(self, filename): def __init__(self, filename):
self.table = Trie() self.table = Trie()
self.load(filename) self.load(filename)
@@ -85,7 +92,7 @@ class Collator:
line = line[:line.find("#")] + "\n" line = line[:line.find("#")] + "\n"
line = line[:line.find("%")] + "\n" line = line[:line.find("%")] + "\n"
line = line.strip() line = line.strip()
if line.startswith("@"): if line.startswith("@"):
pass pass
else: else:
@@ -96,32 +103,36 @@ class Collator:
while True: while True:
begin = x.find("[") begin = x.find("[")
if begin == -1: if begin == -1:
break break
end = x[begin:].find("]") end = x[begin:].find("]")
collElement = x[begin:begin+end+1] collElement = x[begin:begin+end+1]
x = x[begin + 1:] x = x[begin + 1:]
alt = collElement[1] alt = collElement[1]
chars = collElement[2:-1].split(".") chars = collElement[2:-1].split(".")
collElements.append((alt, chars)) collElements.append((alt, chars))
integer_points = [int(ch, 16) for ch in charList] integer_points = [int(ch, 16) for ch in charList]
self.table.add(integer_points, collElements) self.table.add(integer_points, collElements)
def sort_key(self, string): def sort_key(self, string):
collation_elements = [] collation_elements = []
lookup_key = [ord(ch) for ch in string] lookup_key = [ord(ch) for ch in string]
while lookup_key: while lookup_key:
value, lookup_key = self.table.find_prefix(lookup_key) value, lookup_key = self.table.find_prefix(lookup_key)
if not value: if not value:
# @@@ # Calculate implicit weighting for CJK Ideographs
raise ValueError, map(hex, lookup_key) # contributed by David Schneider 2009-07-27
# http://www.unicode.org/reports/tr10/#Implicit_Weights
value = []
value.append((".", ["%X" % (0xFB40 + (lookup_key[0] >> 15)), "0020", "0002", "0001"]))
value.append((".", ["%X" % ((lookup_key[0] & 0x7FFF) | 0x8000), "0000", "0000", "0000"]))
lookup_key = lookup_key[1:]
collation_elements.extend(value) collation_elements.extend(value)
sort_key = [] sort_key = []
for level in range(4): for level in range(4):
if level: if level:
sort_key.append(0) # level separator sort_key.append(0) # level separator
@@ -129,5 +140,5 @@ class Collator:
ce_l = int(element[1][level], 16) ce_l = int(element[1][level], 16)
if ce_l: if ce_l:
sort_key.append(ce_l) sort_key.append(ce_l)
return tuple(sort_key) return tuple(sort_key)
+201
View File
@@ -0,0 +1,201 @@
"""
Developed by niphlod@gmail.com
"""
import redis
from redis.exceptions import ConnectionError
from gluon import current
from gluon.storage import Storage
import cPickle as pickle
import time
import re
import logging
import thread
logger = logging.getLogger("web2py.session.redis")
locker = thread.allocate_lock()
def RedisSession(*args, **vars):
"""
Usage example: put in models
from gluon.contrib.redis_session import RedisSession
sessiondb = RedisSession('localhost:6379',db=0, session_expiry=False)
session.connect(request, response, db = sessiondb)
Simple slip-in storage for session
"""
locker.acquire()
try:
if not hasattr(RedisSession, 'redis_instance'):
RedisSession.redis_instance = RedisClient(*args, **vars)
finally:
locker.release()
return RedisSession.redis_instance
class RedisClient(object):
meta_storage = {}
MAX_RETRIES = 5
RETRIES = 0
def __init__(self, server='localhost:6379', db=None, debug=False, session_expiry=False):
"""session_expiry can be an integer, in seconds, to set the default expiration
of sessions. The corresponding record will be deleted from the redis instance,
and there's virtually no need to run sessions2trash.py
"""
self.server = server
self.db = db or 0
host,port = (self.server.split(':')+['6379'])[:2]
port = int(port)
self.debug = debug
if current and current.request:
self.app = current.request.application
else:
self.app = ''
self.r_server = redis.Redis(host=host, port=port, db=self.db)
self.tablename = None
self.session_expiry = session_expiry
def get(self, what, default):
return self.tablename
def Field(self, fieldname, type='string', length=None, default=None,
required=False,requires=None):
return None
def define_table(self,tablename,*fields,**args):
if not self.tablename:
self.tablename = MockTable(self, self.r_server, tablename, self.session_expiry)
return self.tablename
def __getitem__(self, key):
return self.tablename
def __call__(self, where=''):
q = self.tablename.query
return q
def commit(self):
#this is only called by session2trash.py
pass
class MockTable(object):
def __init__(self, db, r_server, tablename, session_expiry):
self.db = db
self.r_server = r_server
self.tablename = tablename
#set the namespace for sessions of this app
self.keyprefix = 'w2p:sess:%s' % tablename.replace('web2py_session_', '')
#fast auto-increment id (needed for session handling)
self.serial = "%s:serial" % self.keyprefix
#index of all the session keys of this app
self.id_idx = "%s:id_idx" % self.keyprefix
#remember the session_expiry setting
self.session_expiry = session_expiry
def getserial(self):
#return an auto-increment id
return "%s" % self.r_server.incr(self.serial, 1)
def __getattr__(self, key):
if key == 'id':
#return a fake query. We need to query it just by id for normal operations
self.query = MockQuery(field='id', db=self.r_server, prefix=self.keyprefix, session_expiry=self.session_expiry)
return self.query
elif key == '_db':
#needed because of the calls in sessions2trash.py and globals.py
return self.db
def insert(self, **kwargs):
#usually kwargs would be a Storage with several keys:
#'locked', 'client_ip','created_datetime','modified_datetime'
#'unique_key', 'session_data'
#retrieve a new key
newid = self.getserial()
key = "%s:%s" % (self.keyprefix, newid)
#add it to the index
self.r_server.sadd(self.id_idx, key)
#set a hash key with the Storage
self.r_server.hmset(key, kwargs)
if self.session_expiry:
self.r_server.expire(key, self.session_expiry)
return newid
class MockQuery(object):
"""a fake Query object that supports querying by id
and listing all keys. No other operation is supported
"""
def __init__(self, field=None, db=None, prefix=None, session_expiry=False):
self.field = field
self.value = None
self.db = db
self.keyprefix = prefix
self.op = None
self.session_expiry = session_expiry
def __eq__(self, value, op='eq'):
self.value = value
self.op = op
def __gt__(self, value, op='ge'):
self.value = value
self.op = op
def select(self):
if self.op == 'eq' and self.field == 'id' and self.value:
#means that someone wants to retrieve the key self.value
rtn = self.db.hgetall("%s:%s" % (self.keyprefix, self.value))
if rtn == dict():
#return an empty resultset for non existing key
return []
else:
return [Storage(rtn)]
elif self.op == 'ge' and self.field == 'id' and self.value == 0:
#means that someone wants the complete list
rtn = []
id_idx = "%s:id_idx" % self.keyprefix
#find all session keys of this app
allkeys = self.db.smembers(id_idx)
for sess in allkeys:
val = self.db.hgetall(sess)
if val == dict():
if self.session_expiry:
#clean up the idx, because the key expired
self.db.srem(id_idx, sess)
continue
else:
continue
val = Storage(val)
#add a delete_record method (necessary for sessions2trash.py)
val.delete_record = RecordDeleter(self.db, sess, self.keyprefix)
rtn.append(val)
return rtn
else:
raise Exception("Operation not supported")
def update(self, **kwargs):
#means that the session has been found and needs an update
if self.op == 'eq' and self.field == 'id' and self.value:
rtn = self.db.hmset("%s:%s" % (self.keyprefix, self.value), kwargs)
if self.session_expiry:
self.db.expire(key, self.session.expiry)
return rtn
class RecordDeleter(object):
"""Dumb record deleter to support sessions2trash.py"""
def __init__(self, db, key, keyprefix):
self.db, self.key, self.keyprefix = db, key, keyprefix
def __call__(self):
id_idx = "%s:id_idx" % self.keyprefix
#remove from the index
self.db.srem(id_idx, self.key)
#remove the key itself
self.db.delete(self.key)
@@ -12,18 +12,18 @@ Attention: Requires Chrome or Safari. For IE of Firefox you need https://github.
2) start this app: 2) start this app:
python gluon/contrib/comet_messaging.py -k mykey -p 8888 python gluon/contrib/websocket_messaging.py -k mykey -p 8888
3) from any web2py app you can post messages with 3) from any web2py app you can post messages with
from gluon.contrib.comet_messaging import comet_send from gluon.contrib.websocket_messaging import websocket_send
comet_send('http://127.0.0.1:8888','Hello World','mykey','mygroup') websocket_send('http://127.0.0.1:8888','Hello World','mykey','mygroup')
4) from any template you can receive them with 4) from any template you can receive them with
<script> <script>
$(document).ready(function(){ $(document).ready(function(){
if(!web2py_comet('ws://127.0.0.1:8888/realtime/mygroup',function(e){alert(e.data)})) if(!web2py_websocket('ws://127.0.0.1:8888/realtime/mygroup',function(e){alert(e.data)}))
alert("html5 websocket not supported by your browser, try Google Chrome"); alert("html5 websocket not supported by your browser, try Google Chrome");
}); });
</script> </script>
@@ -34,14 +34,14 @@ Or if you want to send json messages and store evaluated json in a var called da
<script> <script>
$(document).ready(function(){ $(document).ready(function(){
var data; var data;
web2py_comet('ws://127.0.0.1:8888/realtime/mygroup',function(e){data=eval('('+e.data+')')}); web2py_websocket('ws://127.0.0.1:8888/realtime/mygroup',function(e){data=eval('('+e.data+')')});
}); });
</script> </script>
- All communications between web2py and comet_messaging will be digitally signed with hmac. - All communications between web2py and websocket_messaging will be digitally signed with hmac.
- All validation is handled on the web2py side and there is no need to modify comet_messaging.py - All validation is handled on the web2py side and there is no need to modify websocket_messaging.py
- Multiple web2py instances can talk with one or more comet_messaging servers. - Multiple web2py instances can talk with one or more websocket_messaging servers.
- "ws://127.0.0.1:8888/realtime/" must be contain the IP of the comet_messaging server. - "ws://127.0.0.1:8888/realtime/" must be contain the IP of the websocket_messaging server.
- Via group='mygroup' name you can support multiple groups of clients (think of many chat-rooms) - Via group='mygroup' name you can support multiple groups of clients (think of many chat-rooms)
Here is a complete sample web2py action: Here is a complete sample web2py action:
@@ -51,7 +51,7 @@ Here is a complete sample web2py action:
script=SCRIPT(''' script=SCRIPT('''
jQuery(document).ready(function(){ jQuery(document).ready(function(){
var callback=function(e){alert(e.data)}; var callback=function(e){alert(e.data)};
if(!web2py_comet('ws://127.0.0.1:8888/realtime/mygroup',callback)) if(!web2py_websocket('ws://127.0.0.1:8888/realtime/mygroup',callback))
alert("html5 websocket not supported by your browser, try Google Chrome"); alert("html5 websocket not supported by your browser, try Google Chrome");
}); });
''') ''')
@@ -60,8 +60,8 @@ Here is a complete sample web2py action:
def ajax_form(): def ajax_form():
form=SQLFORM.factory(Field('message')) form=SQLFORM.factory(Field('message'))
if form.accepts(request,session): if form.accepts(request,session):
from gluon.contrib.comet_messaging import comet_send from gluon.contrib.websocket_messaging import websocket_send
comet_send('http://127.0.0.1:8888',form.vars.message,'mykey','mygroup') websocket_send('http://127.0.0.1:8888',form.vars.message,'mykey','mygroup')
return form return form
Acknowledgements: Acknowledgements:
@@ -83,7 +83,7 @@ listeners = {}
names = {} names = {}
tokens = {} tokens = {}
def comet_send(url,message,hmac_key=None,group='default'): def websocket_send(url,message,hmac_key=None,group='default'):
sig = hmac_key and hmac.new(hmac_key,message).hexdigest() or '' sig = hmac_key and hmac.new(hmac_key,message).hexdigest() or ''
params = urllib.urlencode({'message': message, 'signature': sig, 'group':group}) params = urllib.urlencode({'message': message, 'signature': sig, 'group':group})
f = urllib.urlopen(url, params) f = urllib.urlopen(url, params)
+94 -239
View File
@@ -6,153 +6,137 @@ import os
import re import re
import sys import sys
import threading import threading
import traceback
from gluon import current
# Install the new import function: NATIVE_IMPORTER = __builtin__.__import__
def custom_import_install(web2py_path): INVALID_MODULES = set(('','gluon','applications','custom_import'))
global _web2py_importer
global _web2py_path
if isinstance(__builtin__.__import__, _Web2pyImporter):
return #aready installed
_web2py_path = web2py_path
_web2py_importer = _Web2pyImporter(web2py_path)
__builtin__.__import__ = _web2py_importer
def is_tracking_changes(): # backward compatibility API
""" def custom_import_install():
@return: True: neo_importer is tracking changes made to Python source if __builtin__.__import__ != custom_importer:
files. False: neo_import does not reload Python modules. INVALID_MODULES.update(sys.modules.keys())
""" __builtin__.__import__ = custom_importer
global _is_tracking_changes
return _is_tracking_changes
def track_changes(track=True): def track_changes(track=True):
assert track in (True,False), "must be True or False"
current.request._custom_import_track_changes = track
def is_tracking_changes():
return current.request._custom_import_track_changes
class CustomImportException(ImportError):
pass
def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1):
""" """
Tell neo_importer to start/stop tracking changes made to Python modules. The web2py custom importer. Like the standard Python importer but it
@param track: True: Start tracking changes. False: Stop tracking changes. tries to transform import statements as something like
"import applications.app_name.modules.x".
If the import failed, fall back on naive_importer
""" """
global _is_tracking_changes globals = globals or {}
global _web2py_importer locals = locals or {}
global _web2py_date_tracker_importer fromlist = fromlist or []
assert track is True or track is False, "Boolean expected."
if track == _is_tracking_changes:
return
if track:
if not _web2py_date_tracker_importer:
_web2py_date_tracker_importer = \
_Web2pyDateTrackerImporter(_web2py_path)
__builtin__.__import__ = _web2py_date_tracker_importer
else:
__builtin__.__import__ = _web2py_importer
_is_tracking_changes = track
_STANDARD_PYTHON_IMPORTER = __builtin__.__import__ # Keep standard importer try:
_web2py_importer = None # The standard web2py importer if current.request._custom_import_track_changes:
_web2py_date_tracker_importer = None # The web2py importer with date tracking base_importer = TRACK_IMPORTER
_web2py_path = None # Absolute path of the web2py directory else:
base_importer = NATIVE_IMPORTER
except: # there is no current.request (should never happen)
base_importer = NATIVE_IMPORTER
_is_tracking_changes = False # The tracking mode # if not relative and not from applications:
if hasattr(current,'request') \
and level<=0 \
and not name.split('.')[0] in INVALID_MODULES \
and isinstance(globals, dict):
import_tb = None
try:
items = current.request.folder.split(os.path.sep)
if not items[-1]: items = items[:-1]
modules_prefix = '.'.join(items[-2:])+'.modules'
if not fromlist:
# import like "import x" or "import x.y"
result = None
for itemname in name.split("."):
new_mod = base_importer(
modules_prefix, globals,locals, [itemname], level)
try:
result = result or new_mod.__dict__[itemname]
except KeyError, e:
raise ImportError, 'Cannot import module %s' % str(e)
modules_prefix += "." + itemname
return result
else:
# import like "from x import a, b, ..."
pname = modules_prefix + "." + name
return base_importer(pname, globals, locals, fromlist, level)
except ImportError, e1:
import_tb = sys.exc_info()[2]
try:
return NATIVE_IMPORTER(name,globals,locals,fromlist,level)
except ImportError, e3:
raise ImportError, e1, import_tb # there an import error in the module
except Exception, e2:
raise e2 # there is an error in the module
finally:
if import_tb:
import_tb = None
class _BaseImporter(object): return NATIVE_IMPORTER(name,globals,locals,fromlist,level)
"""
The base importer. Dispatch the import the call to the standard Python
importer.
"""
def begin(self):
"""
Many imports can be made for a single import statement. This method
help the management of this aspect.
"""
def __init__(self):
self._STANDARD_PYTHON_IMPORTER = _STANDARD_PYTHON_IMPORTER
def __call__(self, name, globals=None, locals=None,
fromlist=None, level=-1):
"""
The import method itself.
"""
return self._STANDARD_PYTHON_IMPORTER(name,
globals,
locals,
fromlist,
level)
def end(self):
"""
Needed for clean up.
"""
class _DateTrackerImporter(_BaseImporter): class TrackImporter(object):
""" """
An importer tracking the date of the module files and reloading them when An importer tracking the date of the module files and reloading them when
they have changed. they have changed.
""" """
_PACKAGE_PATH_SUFFIX = os.path.sep+"__init__.py" THREAD_LOCAL = threading.local()
PACKAGE_PATH_SUFFIX = os.path.sep+"__init__.py"
def __init__(self): def __init__(self):
super(_DateTrackerImporter, self).__init__()
self._import_dates = {} # Import dates of the files of the modules self._import_dates = {} # Import dates of the files of the modules
# Avoid reloading cause by file modifications of reload:
self._tl = threading.local()
self._tl._modules_loaded = None
def begin(self): def __call__(self,name,globals=None,locals=None,fromlist=None,level=-1):
self._tl._modules_loaded = set()
def __call__(self, name, globals=None, locals=None,
fromlist=None, level=-1):
""" """
The import method itself. The import method itself.
""" """
globals = globals or {} globals = globals or {}
locals = locals or {} locals = locals or {}
fromlist = fromlist or [] fromlist = fromlist or []
if not hasattr(self.THREAD_LOCAL,'_modules_loaded'):
call_begin_end = self._tl._modules_loaded is None self.THREAD_LOCAL._modules_loaded = set()
if call_begin_end:
self.begin()
try: try:
self._tl.globals = globals
self._tl.locals = locals
self._tl.level = level
# Check the date and reload if needed: # Check the date and reload if needed:
self._update_dates(name, fromlist) self._update_dates(name, globals, locals, fromlist, level)
# Try to load the module and update the dates if it works: # Try to load the module and update the dates if it works:
result = super(_DateTrackerImporter, self) \ result = NATIVE_IMPORTER(name, globals, locals, fromlist, level)
.__call__(name, globals, locals, fromlist, level)
# Module maybe loaded for the 1st time so we need to set the date # Module maybe loaded for the 1st time so we need to set the date
self._update_dates(name, fromlist) self._update_dates(name, globals, locals, fromlist, level)
return result return result
except Exception: except Exception, e:
raise # Don't hide something that went wrong raise # Don't hide something that went wrong
finally:
if call_begin_end:
self.end()
def _update_dates(self, name, fromlist): def _update_dates(self, name, globals, locals, fromlist, level):
""" """
Update all the dates associated to the statement import. A single Update all the dates associated to the statement import. A single
import statement may import many modules. import statement may import many modules.
""" """
self._reload_check(name) self._reload_check(name, globals, locals, level)
if fromlist: for fromlist_name in fromlist or []:
for fromlist_name in fromlist: pname = "%s.%s" % (name, fromlist_name)
self._reload_check("%s.%s" % (name, fromlist_name)) self._reload_check(pname, globals, locals, level)
def _reload_check(self, name): def _reload_check(self, name, globals, locals, level):
""" """
Update the date associated to the module and reload the module if Update the date associated to the module and reload the module if
the file has changed. the file has changed.
""" """
module = sys.modules.get(name) module = sys.modules.get(name)
file = self._get_module_file(module) file = self._get_module_file(module)
if file: if file:
@@ -170,7 +154,7 @@ class _DateTrackerImporter(_BaseImporter):
# Get path without file ext: # Get path without file ext:
file = os.path.splitext(file)[0] file = os.path.splitext(file)[0]
reload_mod = os.path.isdir(file) \ reload_mod = os.path.isdir(file) \
and os.path.isfile(file+self._PACKAGE_PATH_SUFFIX) and os.path.isfile(file+self.PACKAGE_PATH_SUFFIX)
mod_to_pack = reload_mod mod_to_pack = reload_mod
else: # Package turning into module? else: # Package turning into module?
file += ".py" file += ".py"
@@ -180,156 +164,27 @@ class _DateTrackerImporter(_BaseImporter):
if reload_mod or not date or new_date > date: if reload_mod or not date or new_date > date:
self._import_dates[file] = new_date self._import_dates[file] = new_date
if reload_mod or (date and new_date > date): if reload_mod or (date and new_date > date):
if module not in self._tl._modules_loaded: if module not in self.THREAD_LOCAL._modules_loaded:
if mod_to_pack: if mod_to_pack:
# Module turning into a package: # Module turning into a package:
mod_name = module.__name__ mod_name = module.__name__
del sys.modules[mod_name] # Delete the module del sys.modules[mod_name] # Delete the module
# Reload the module: # Reload the module:
super(_DateTrackerImporter, self).__call__ \ NATIVE_IMPORTER(mod_name, globals, locals, [], level)
(mod_name, self._tl.globals, self._tl.locals, [],
self._tl.level)
else: else:
reload(module) reload(module)
self._tl._modules_loaded.add(module) self.THREAD_LOCAL._modules_loaded.add(module)
def end(self): def _get_module_file(self, module):
self._tl._modules_loaded = None
@classmethod
def _get_module_file(cls, module):
""" """
Get the absolute path file associated to the module or None. Get the absolute path file associated to the module or None.
""" """
file = getattr(module, "__file__", None) file = getattr(module, "__file__", None)
if file: if file:
# Make path absolute if not: # Make path absolute if not:
#file = os.path.join(cls.web2py_path, file)
file = os.path.splitext(file)[0]+".py" # Change .pyc for .py file = os.path.splitext(file)[0]+".py" # Change .pyc for .py
if file.endswith(cls._PACKAGE_PATH_SUFFIX): if file.endswith(self.PACKAGE_PATH_SUFFIX):
file = os.path.dirname(file) # Track dir for packages file = os.path.dirname(file) # Track dir for packages
return file return file
class _Web2pyImporter(_BaseImporter): TRACK_IMPORTER = TrackImporter()
"""
The standard web2py importer. Like the standard Python importer but it
tries to transform import statements as something like
"import applications.app_name.modules.x". If the import failed, fall back
on _BaseImporter.
"""
_RE_ESCAPED_PATH_SEP = re.escape(os.path.sep) # os.path.sep escaped for re
def __init__(self, web2py_path):
"""
@param web2py_path: The absolute path of the web2py installation.
"""
global DEBUG
self.super_class = super(_Web2pyImporter, self)
self.super_class.__init__()
self.web2py_path = web2py_path
self.__web2py_path_os_path_sep = self.web2py_path+os.path.sep
self.__web2py_path_os_path_sep_len = len(self.__web2py_path_os_path_sep)
self.__RE_APP_DIR = re.compile(
self._RE_ESCAPED_PATH_SEP.join( \
( \
#"^" + re.escape(web2py_path), # Not working with Python 2.5
"^(" + "applications",
"[^",
"]+)",
"",
) ))
def _matchAppDir(self, file_path):
"""
Does the file in a directory inside the "applications" directory?
"""
if file_path.startswith(self.__web2py_path_os_path_sep):
file_path = file_path[self.__web2py_path_os_path_sep_len:]
return self.__RE_APP_DIR.match(file_path)
return False
def __call__(self, name, globals=None, locals=None,
fromlist=None, level=-1):
"""
The import method itself.
"""
globals = globals or {}
locals = locals or {}
fromlist = fromlist or []
self.begin()
#try:
# if not relative and not from applications:
if not name.startswith(".") and level <= 0 \
and not name.startswith("applications.") \
and isinstance(globals, dict):
# Get the name of the file do the import
caller_file_name = os.path.join(self.web2py_path, \
globals.get("__file__", ""))
# Is the path in an application directory?
match_app_dir = self._matchAppDir(caller_file_name)
if match_app_dir:
try:
# Get the prefix to add for the import
# (like applications.app_name.modules):
modules_prefix = \
".".join((match_app_dir.group(1). \
replace(os.path.sep, "."), "modules"))
if not fromlist:
# import like "import x" or "import x.y"
return self.__import__dot(modules_prefix, name,
globals, locals, fromlist, level)
else:
# import like "from x import a, b, ..."
return self.super_class \
.__call__(modules_prefix+"."+name,
globals, locals, fromlist, level)
except ImportError, e:
try:
return self.super_class.__call__(name, globals, locals,
fromlist, level)
except ImportError, e1:
raise e
return self.super_class.__call__(name, globals, locals,
fromlist, level)
def __import__dot(self, prefix, name, globals, locals, fromlist,
level):
"""
Here we will import x.y.z as many imports like:
from applications.app_name.modules import x
from applications.app_name.modules.x import y
from applications.app_name.modules.x.y import z.
x will be the module returned.
"""
result = None
for name in name.split("."):
new_mod = super(_Web2pyImporter, self).__call__(prefix, globals,
locals, [name], level)
try:
result = result or new_mod.__dict__[name]
except KeyError, e:
raise ImportError, 'Cannot import module %s' % str(e)
prefix += "." + name
return result
class _Web2pyDateTrackerImporter(_Web2pyImporter, _DateTrackerImporter):
"""
Like _Web2pyImporter but using a _DateTrackerImporter.
"""
+620 -567
View File
File diff suppressed because it is too large Load Diff
+33 -21
View File
@@ -128,6 +128,7 @@ class Request(Storage):
and secure the session. and secure the session.
""" """
if not global_settings.cronjob and not self.is_https: if not global_settings.cronjob and not self.is_https:
current.session.forget()
redirect(URL(scheme='https', args=self.args, vars=self.vars)) redirect(URL(scheme='https', args=self.args, vars=self.vars))
current.session.secure() current.session.secure()
@@ -229,7 +230,7 @@ class Response(Storage):
for k,v in (self.meta or {}).iteritems()) for k,v in (self.meta or {}).iteritems())
self.write(s,escape=False) self.write(s,escape=False)
def include_files(self): def include_files(self, extensions=None):
""" """
Caching method for writing out files. Caching method for writing out files.
@@ -240,11 +241,22 @@ class Response(Storage):
from gluon import URL from gluon import URL
files = [] files = []
has_js = has_css = False
for item in self.files: for item in self.files:
if not item in files: files.append(item) if extensions and not item.split('.')[-1] in extensions:
if have_minify and (self.optimize_css or self.optimize_js): continue
if item in files:
continue
if item.endswith('.js'):
has_js = True
if item.endswith('.css'):
has_css = True
files.append(item)
if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)):
# cache for 5 minutes by default # cache for 5 minutes by default
key = hashlib.md5(repr(files)).hexdigest() key = hashlib.md5(repr(files)).hexdigest()
cache = self.cache_includes or (current.cache.ram, 60*5) cache = self.cache_includes or (current.cache.ram, 60*5)
def call_minify(files=files): def call_minify(files=files):
return minify.minify(files, return minify.minify(files,
@@ -263,6 +275,8 @@ class Response(Storage):
for item in files: for item in files:
if isinstance(item,str): if isinstance(item,str):
f = item.lower().split('?')[0] f = item.lower().split('?')[0]
if self.static_version:
item = item.replace('/static/', '/static/_%s/' % self.static_version, 1)
if f.endswith('.css'): s += css_template % item if f.endswith('.css'): s += css_template % item
elif f.endswith('.js'): s += js_template % item elif f.endswith('.js'): s += js_template % item
elif f.endswith('.coffee'): s += coffee_template % item elif f.endswith('.coffee'): s += coffee_template % item
@@ -316,7 +330,8 @@ class Response(Storage):
stream_file_or_304_or_206(stream, stream_file_or_304_or_206(stream,
chunk_size=chunk_size, chunk_size=chunk_size,
request=request, request=request,
headers=headers) headers=headers,
status=self.status)
# ## the following is for backward compatibility # ## the following is for backward compatibility
if hasattr(stream, 'name'): if hasattr(stream, 'name'):
@@ -402,15 +417,15 @@ class Response(Storage):
return handler(request, self, methods) return handler(request, self, methods)
def toolbar(self): def toolbar(self):
from html import DIV, SCRIPT, BEAUTIFY, TAG, URL from html import DIV, SCRIPT, BEAUTIFY, TAG, URL, A
BUTTON = TAG.button BUTTON = TAG.button
admin = URL("admin","default","design", admin = URL("admin","default","design",
args=current.request.application) args=current.request.application)
from gluon.dal import thread from gluon.dal import THREAD_LOCAL
if hasattr(thread,'instances'): if hasattr(THREAD_LOCAL,'instances'):
dbstats = [TABLE(*[TR(PRE(row[0]),'%.2fms' % (row[1]*1000)) \ dbstats = [TABLE(*[TR(PRE(row[0]),'%.2fms' % (row[1]*1000)) \
for row in i.db._timings]) \ for row in i.db._timings]) \
for i in thread.instances] for i in THREAD_LOCAL.instances]
dbtables = dict([(regex_nopasswd.sub('******',i.uri), dbtables = dict([(regex_nopasswd.sub('******',i.uri),
{'defined': {'defined':
sorted(list(set(i.db.tables) - sorted(list(set(i.db.tables) -
@@ -418,24 +433,26 @@ class Response(Storage):
'[no defined tables]', '[no defined tables]',
'lazy': sorted(i.db._LAZY_TABLES.keys()) or 'lazy': sorted(i.db._LAZY_TABLES.keys()) or
'[no lazy tables]'}) '[no lazy tables]'})
for i in thread.instances]) for i in THREAD_LOCAL.instances])
else: else:
dbstats = [] # if no db or on GAE dbstats = [] # if no db or on GAE
dbtables = {} dbtables = {}
u = web2py_uuid() u = web2py_uuid()
backtotop = A('Back to top', _href="#totop-%s" % u)
return DIV( return DIV(
BUTTON('design',_onclick="document.location='%s'" % admin), BUTTON('design',_onclick="document.location='%s'" % admin),
BUTTON('request',_onclick="jQuery('#request-%s').slideToggle()"%u), BUTTON('request',_onclick="jQuery('#request-%s').slideToggle()"%u),
DIV(BEAUTIFY(current.request),_class="hidden",_id="request-%s"%u),
BUTTON('session',_onclick="jQuery('#session-%s').slideToggle()"%u),
DIV(BEAUTIFY(current.session),_class="hidden",_id="session-%s"%u),
BUTTON('response',_onclick="jQuery('#response-%s').slideToggle()"%u), BUTTON('response',_onclick="jQuery('#response-%s').slideToggle()"%u),
DIV(BEAUTIFY(current.response),_class="hidden",_id="response-%s"%u), BUTTON('session',_onclick="jQuery('#session-%s').slideToggle()"%u),
BUTTON('db tables',_onclick="jQuery('#db-tables-%s').slideToggle()"%u), BUTTON('db tables',_onclick="jQuery('#db-tables-%s').slideToggle()"%u),
DIV(BEAUTIFY(dbtables),_class="hidden",_id="db-tables-%s"%u),
BUTTON('db stats',_onclick="jQuery('#db-stats-%s').slideToggle()"%u), BUTTON('db stats',_onclick="jQuery('#db-stats-%s').slideToggle()"%u),
DIV(BEAUTIFY(dbstats),_class="hidden",_id="db-stats-%s"%u), DIV(BEAUTIFY(current.request), backtotop,_class="hidden",_id="request-%s"%u),
DIV(BEAUTIFY(current.session), backtotop, _class="hidden",_id="session-%s"%u),
DIV(BEAUTIFY(current.response), backtotop, _class="hidden",_id="response-%s"%u),
DIV(BEAUTIFY(dbtables), backtotop, _class="hidden",_id="db-tables-%s"%u),
DIV(BEAUTIFY(dbstats), backtotop, _class="hidden",_id="db-stats-%s"%u),
SCRIPT("jQuery('.hidden').hide()") SCRIPT("jQuery('.hidden').hide()")
,_id="totop-%s" % u
) )
class Session(Storage): class Session(Storage):
@@ -467,7 +484,7 @@ class Session(Storage):
if not masterapp: if not masterapp:
masterapp = request.application masterapp = request.application
response.session_id_name = 'session_id_%s' % masterapp.lower() response.session_id_name = 'session_id_%s' % masterapp.lower()
# Load session data from cookie # Load session data from cookie
cookies = request.cookies cookies = request.cookies
@@ -703,8 +720,3 @@ class Session(Storage):
del response.session_file del response.session_file
except: except:
pass pass
+38 -16
View File
@@ -32,6 +32,11 @@ regex_crlf = re.compile('\r|\n')
join = ''.join join = ''.join
# name2codepoint is incomplete respect to xhtml (and xml): 'apos' is missing.
entitydefs = dict(map(lambda (k,v): (k, unichr(v).encode('utf-8')), name2codepoint.iteritems()))
entitydefs.setdefault('apos', u"'".encode('utf-8'))
__all__ = [ __all__ = [
'A', 'A',
'B', 'B',
@@ -243,7 +248,7 @@ def URL(
elif a and c and not f: (c,f,a)=(a,c,f) elif a and c and not f: (c,f,a)=(a,c,f)
from globals import current from globals import current
if hasattr(current,'request'): if hasattr(current,'request'):
r = current.request r = current.request
if r: if r:
application = r.application application = r.application
@@ -487,7 +492,7 @@ class XmlComponent(object):
components += [other] components += [other]
return CAT(*components) return CAT(*components)
def add_class(self, name): def add_class(self, name):
""" add a class to _class attribute """ """ add a class to _class attribute """
c = self['_class'] c = self['_class']
classes = (set(c.split()) if c else set())|set(name.split()) classes = (set(c.split()) if c else set())|set(name.split())
@@ -1745,7 +1750,7 @@ class INPUT(DIV):
elif not t == 'submit': elif not t == 'submit':
if value is None: if value is None:
self['value'] = _value self['value'] = _value
else: elif not isinstance(value,list):
self['_value'] = value self['_value'] = value
def xml(self): def xml(self):
@@ -1938,6 +1943,7 @@ class FORM(DIV):
# check formname and formkey # check formname and formkey
status = True status = True
changed = False
request_vars = self.request_vars request_vars = self.request_vars
if session: if session:
formkey = session.get('_formkey[%s]' % formname, None) formkey = session.get('_formkey[%s]' % formname, None)
@@ -1950,18 +1956,23 @@ class FORM(DIV):
# check if editing a record that has been modified by the server # check if editing a record that has been modified by the server
if hasattr(self,'record_hash') and self.record_hash != formkey: if hasattr(self,'record_hash') and self.record_hash != formkey:
status = False status = False
self.record_changed = True self.record_changed = changed = True
status = self._traverse(status,hideerror) status = self._traverse(status,hideerror)
status = self.assert_status(status, request_vars) status = self.assert_status(status, request_vars)
if onvalidation: if onvalidation:
if isinstance(onvalidation, dict): if isinstance(onvalidation, dict):
onsuccess = onvalidation.get('onsuccess', None) onsuccess = onvalidation.get('onsuccess', None)
onfailure = onvalidation.get('onfailure', None) onfailure = onvalidation.get('onfailure', None)
onchange = onvalidation.get('onchange', None)
if onsuccess and status: if onsuccess and status:
onsuccess(self) onsuccess(self)
if onfailure and request_vars and not status: if onfailure and request_vars and not status:
onfailure(self) onfailure(self)
status = len(self.errors) == 0 status = len(self.errors) == 0
if changed:
if onchange and self.record_changed and \
self.detect_record_change:
onchange(self)
elif status: elif status:
if isinstance(onvalidation, (list, tuple)): if isinstance(onvalidation, (list, tuple)):
[f(self) for f in onvalidation] [f(self) for f in onvalidation]
@@ -2030,8 +2041,13 @@ class FORM(DIV):
onfailure = 'flash' - will show message_onfailure in response.flash onfailure = 'flash' - will show message_onfailure in response.flash
None - will do nothing None - will do nothing
can be a function (lambda form: pass) can be a function (lambda form: pass)
onchange = 'flash' - will show message_onchange in response.flash
None - will do nothing
can be a function (lambda form: pass)
message_onsuccess message_onsuccess
message_onfailure message_onfailure
message_onchange
next = where to redirect in case of success next = where to redirect in case of success
any other kwargs will be passed for form.accepts(...) any other kwargs will be passed for form.accepts(...)
""" """
@@ -2042,13 +2058,17 @@ class FORM(DIV):
onsuccess = kwargs.get('onsuccess','flash') onsuccess = kwargs.get('onsuccess','flash')
onfailure = kwargs.get('onfailure','flash') onfailure = kwargs.get('onfailure','flash')
onchange = kwargs.get('onchange', 'flash')
message_onsuccess = kwargs.get('message_onsuccess', message_onsuccess = kwargs.get('message_onsuccess',
current.T("Success!")) current.T("Success!"))
message_onfailure = kwargs.get('message_onfailure', message_onfailure = kwargs.get('message_onfailure',
current.T("Errors in form, please check it out.")) current.T("Errors in form, please check it out."))
message_onchange = kwargs.get('message_onchange',
current.T("Form consecutive submissions not allowed. " +
"Try re-submitting or refreshing the form page."))
next = kwargs.get('next',None) next = kwargs.get('next',None)
for key in ('message_onsuccess','message_onfailure','onsuccess', for key in ('message_onsuccess','message_onfailure','onsuccess',
'onfailure','next'): 'onfailure','next', 'message_onchange', 'onchange'):
if key in kwargs: if key in kwargs:
del kwargs[key] del kwargs[key]
@@ -2075,6 +2095,13 @@ class FORM(DIV):
elif callable(onfailure): elif callable(onfailure):
onfailure(self) onfailure(self)
return False return False
elif hasattr(self, "record_changed"):
if self.record_changed and self.detect_record_change:
if onchange == 'flash':
current.response.flash = message_onchange
elif callable(onchange):
onchange(self)
return False
def process(self, **kwargs): def process(self, **kwargs):
""" """
@@ -2111,8 +2138,10 @@ class FORM(DIV):
REDIRECT_JS = "window.location='%s';return false" REDIRECT_JS = "window.location='%s';return false"
def add_button(self,value,url,_class=None): def add_button(self,value,url,_class=None):
self[0][-1][1].append(INPUT(_type="button",_value=value,_class=_class, submit = self.element('input[type=submit]')
_onclick=self.REDIRECT_JS % url)) submit.parent.append(
INPUT(_type="button",_value=value,_class=_class,
_onclick=self.REDIRECT_JS % url))
@@ -2276,7 +2305,7 @@ class MENU(DIV):
select = SELECT(**self.attributes) select = SELECT(**self.attributes)
for item in data: for item in data:
if len(item) <= 4 or item[4] == True: if len(item) <= 4 or item[4] == True:
select.append(OPTION(CAT(prefix, item[0]), select.append(OPTION(CAT(prefix, item[0]),
_value=item[2], _selected=item[1])) _value=item[2], _selected=item[1]))
if len(item)>3 and len(item[3]): if len(item)>3 and len(item[3]):
self.serialize_mobile( self.serialize_mobile(
@@ -2402,7 +2431,7 @@ class web2pyHTMLParser(HTMLParser):
else: else:
self.parent.append(unichr(int(name)).encode('utf8')) self.parent.append(unichr(int(name)).encode('utf8'))
def handle_entityref(self,name): def handle_entityref(self,name):
self.parent.append(unichr(name2codepoint[name]).encode('utf8')) self.parent.append(entitydefs[name])
def handle_endtag(self, tagname): def handle_endtag(self, tagname):
# this deals with unbalanced tags # this deals with unbalanced tags
if tagname==self.last: if tagname==self.last:
@@ -2506,10 +2535,3 @@ class MARKMIN(XmlComponent):
if __name__ == '__main__': if __name__ == '__main__':
import doctest import doctest
doctest.testmod() doctest.testmod()
+1
View File
@@ -42,6 +42,7 @@ defined_status = {
415: 'UNSUPPORTED MEDIA TYPE', 415: 'UNSUPPORTED MEDIA TYPE',
416: 'REQUESTED RANGE NOT SATISFIABLE', 416: 'REQUESTED RANGE NOT SATISFIABLE',
417: 'EXPECTATION FAILED', 417: 'EXPECTATION FAILED',
422: 'UNPROCESSABLE ENTITY',
500: 'INTERNAL SERVER ERROR', 500: 'INTERNAL SERVER ERROR',
501: 'NOT IMPLEMENTED', 501: 'NOT IMPLEMENTED',
502: 'BAD GATEWAY', 502: 'BAD GATEWAY',
+2 -2
View File
@@ -155,9 +155,9 @@ def read_possible_plural_rules():
create list of all possible plural rules files create list of all possible plural rules files
result is cached in PLURAL_RULES dictionary to increase speed result is cached in PLURAL_RULES dictionary to increase speed
""" """
plurals = {}
try: try:
import gluon.contrib.plural_rules as package import contrib.plural_rules as package
plurals = {}
for importer, modname, ispkg in pkgutil.iter_modules(package.__path__): for importer, modname, ispkg in pkgutil.iter_modules(package.__path__):
if len(modname)==2: if len(modname)==2:
module = __import__(package.__name__+'.'+modname, module = __import__(package.__name__+'.'+modname,
+49 -36
View File
@@ -35,8 +35,6 @@ from settings import global_settings
from admin import add_path_first, create_missing_folders, create_missing_app_folders from admin import add_path_first, create_missing_folders, create_missing_app_folders
from globals import current from globals import current
from custom_import import custom_import_install
# Remarks: # Remarks:
# calling script has inserted path to script directory into sys.path # calling script has inserted path to script directory into sys.path
# applications_parent (path to applications/, site-packages/ etc) # applications_parent (path to applications/, site-packages/ etc)
@@ -55,8 +53,6 @@ from custom_import import custom_import_install
web2py_path = global_settings.applications_parent # backward compatibility web2py_path = global_settings.applications_parent # backward compatibility
custom_import_install(web2py_path)
create_missing_folders() create_missing_folders()
# set up logging for subsequent imports # set up logging for subsequent imports
@@ -88,10 +84,10 @@ from contenttype import contenttype
from dal import BaseAdapter from dal import BaseAdapter
from settings import global_settings from settings import global_settings
from validators import CRYPT from validators import CRYPT
from cache import Cache from cache import CacheInRam
from html import URL, xmlescape from html import URL, xmlescape
from utils import is_valid_ip_address from utils import is_valid_ip_address
from rewrite import load, url_in, thread as rwthread, \ from rewrite import load, url_in, THREAD_LOCAL as rwthread, \
try_rewrite_on_error, fixup_missing_path_info try_rewrite_on_error, fixup_missing_path_info
import newcron import newcron
@@ -123,6 +119,8 @@ except:
load() load()
HTTPS_SCHEMES = set(('https','HTTPS'))
def get_client(env): def get_client(env):
""" """
guess the client address from the environment variables guess the client address from the environment variables
@@ -132,7 +130,7 @@ def get_client(env):
""" """
g = regex_client.search(env.get('http_x_forwarded_for', '')) g = regex_client.search(env.get('http_x_forwarded_for', ''))
if g: if g:
client = g.group() client = (g.group() or '').split(',')[0]
else: else:
g = regex_client.search(env.get('remote_addr', '')) g = regex_client.search(env.get('remote_addr', ''))
if g: if g:
@@ -146,29 +144,32 @@ def get_client(env):
def copystream_progress(request, chunk_size= 10**5): def copystream_progress(request, chunk_size= 10**5):
""" """
copies request.env.wsgi_input into request.body copies request.env.wsgi_input into request.body
and stores progress upload status in cache.ram and stores progress upload status in cache_ram
X-Progress-ID:length and X-Progress-ID:uploaded X-Progress-ID:length and X-Progress-ID:uploaded
""" """
env = request.env env = request.env
if not env.content_length: if not env.content_length:
return cStringIO.StringIO() return cStringIO.StringIO()
source = env.wsgi_input source = env.wsgi_input
size = int(env.content_length) try:
size = int(env.content_length)
except ValueError:
raise HTTP(400,"Invalid Content-Length header")
dest = tempfile.TemporaryFile() dest = tempfile.TemporaryFile()
if not 'X-Progress-ID' in request.vars: if not 'X-Progress-ID' in request.vars:
copystream(source, dest, size, chunk_size) copystream(source, dest, size, chunk_size)
return dest return dest
cache_key = 'X-Progress-ID:'+request.vars['X-Progress-ID'] cache_key = 'X-Progress-ID:'+request.vars['X-Progress-ID']
cache = Cache(request) cache_ram = CacheInRam(request) # same as cache.ram because meta_storage
cache.ram(cache_key+':length', lambda: size, 0) cache_ram(cache_key+':length', lambda: size, 0)
cache.ram(cache_key+':uploaded', lambda: 0, 0) cache_ram(cache_key+':uploaded', lambda: 0, 0)
while size > 0: while size > 0:
if size < chunk_size: if size < chunk_size:
data = source.read(size) data = source.read(size)
cache.ram.increment(cache_key+':uploaded', size) cache_ram.increment(cache_key+':uploaded', size)
else: else:
data = source.read(chunk_size) data = source.read(chunk_size)
cache.ram.increment(cache_key+':uploaded', chunk_size) cache_ram.increment(cache_key+':uploaded', chunk_size)
length = len(data) length = len(data)
if length > size: if length > size:
(data, length) = (data[:size], size) (data, length) = (data[:size], size)
@@ -179,8 +180,8 @@ def copystream_progress(request, chunk_size= 10**5):
if length < chunk_size: if length < chunk_size:
break break
dest.seek(0) dest.seek(0)
cache.ram(cache_key+':length', None) cache_ram(cache_key+':length', None)
cache.ram(cache_key+':uploaded', None) cache_ram(cache_key+':uploaded', None)
return dest return dest
@@ -325,10 +326,12 @@ def parse_get_post_vars(request, environ):
dpk = dpost[key] dpk = dpost[key]
# if en element is not a file replace it with its value else leave it alone # if en element is not a file replace it with its value else leave it alone
if isinstance(dpk, list): if isinstance(dpk, list):
if not dpk[0].filename: value = []
value = [x.value for x in dpk] for _dpk in dpk:
else: if not _dpk.filename:
value = [x for x in dpk] value.append(_dpk.value)
else:
value.append(_dpk)
elif not dpk.filename: elif not dpk.filename:
value = dpk.value value = dpk.value
else: else:
@@ -400,7 +403,7 @@ def wsgibase(environ, responder):
# ################################################## # ##################################################
fixup_missing_path_info(environ) fixup_missing_path_info(environ)
(static_file, environ) = url_in(request, environ) (static_file, version, environ) = url_in(request, environ)
response.status = env.web2py_status_code or response.status response.status = env.web2py_status_code or response.status
if static_file: if static_file:
@@ -408,24 +411,34 @@ def wsgibase(environ, responder):
'attachment'): 'attachment'):
response.headers['Content-Disposition'] \ response.headers['Content-Disposition'] \
= 'attachment' = 'attachment'
if version:
response.headers['Cache-Control'] = 'max-age=315360000'
response.headers['Expires'] = 'Thu, 31 Dec 2037 23:59:59 GMT'
response.stream(static_file, request=request) response.stream(static_file, request=request)
# ################################################## # ##################################################
# fill in request items # fill in request items
# ################################################## # ##################################################
app = request.application ## must go after url_in! app = request.application ## must go after url_in!
http_host = env.http_host.split(':',1)[0] if not global_settings.local_hosts:
local_hosts = [http_host,'::1','127.0.0.1', local_hosts = ['127.0.0.1','::ffff:127.0.0.1']
'::ffff:127.0.0.1'] if not global_settings.web2py_runtime_gae:
if not global_settings.web2py_runtime_gae: try:
local_hosts.append(socket.gethostname()) local_hosts.append(socket.gethostname())
try: except TypeError:
local_hosts.append( pass
socket.gethostbyname(http_host)) try:
except socket.gaierror: if env.server_name:
pass local_hosts += [
client = get_client(env) env.server_name,
socket.gethostbyname(env.server_name)]
except (socket.gaierror,TypeError):
pass
global_settings.local_hosts = local_hosts
else:
local_hosts = global_settings.local_hosts
client = get_client(env)
x_req_with = str(env.http_x_requested_with).lower() x_req_with = str(env.http_x_requested_with).lower()
request.update( request.update(
@@ -434,8 +447,9 @@ def wsgibase(environ, responder):
ajax = x_req_with == 'xmlhttprequest', ajax = x_req_with == 'xmlhttprequest',
cid = env.http_web2py_component_element, cid = env.http_web2py_component_element,
is_local = env.remote_addr in local_hosts, is_local = env.remote_addr in local_hosts,
is_https = env.wsgi_url_scheme \ is_https = env.wsgi_url_scheme in HTTPS_SCHEMES \
in ['https', 'HTTPS'] or env.https=='on') or request.env.http_x_forwarded_proto in HTTPS_SCHEMES \
or env.https=='on')
request.uuid = request.compute_uuid() # requires client request.uuid = request.compute_uuid() # requires client
request.url = environ['PATH_INFO'] request.url = environ['PATH_INFO']
@@ -781,7 +795,6 @@ class HttpServer(object):
global_settings.applications_parent = path global_settings.applications_parent = path
os.chdir(path) os.chdir(path)
[add_path_first(p) for p in (path, abspath('site-packages'), "")] [add_path_first(p) for p in (path, abspath('site-packages'), "")]
custom_import_install(web2py_path)
if exists("logging.conf"): if exists("logging.conf"):
logging.config.fileConfig("logging.conf") logging.config.fileConfig("logging.conf")
+7 -3
View File
@@ -314,22 +314,26 @@ def crondance(applications_parent, ctype='soft', startup=False, apps=None):
(action,models,command) = (True,'-M',command[1:]) (action,models,command) = (True,'-M',command[1:])
else: else:
action=False action=False
if action and command.endswith('.py'): if action and command.endswith('.py'):
commands.extend(('-J', # cron job commands.extend(('-J', # cron job
models, # import models? models, # import models?
'-S', app, # app name '-S', app, # app name
'-a', '"<recycle>"', # password '-a', '"<recycle>"', # password
'-R', command)) # command '-R', command)) # command
shell = True
elif action: elif action:
commands.extend(('-J', # cron job commands.extend(('-J', # cron job
models, # import models? models, # import models?
'-S', app+'/'+command, # app name '-S', app+'/'+command, # app name
'-a', '"<recycle>"')) # password '-a', '"<recycle>"')) # password
shell = True
else: else:
commands = command commands = command
shell = False
# from python docs:
# You do not need shell=True to run a batch file or
# console-based executable.
shell = False
try: try:
cronlauncher(commands, shell=shell).start() cronlauncher(commands, shell=shell).start()
except Exception, e: except Exception, e:
+3 -2
View File
@@ -140,8 +140,9 @@ class LockedFile(object):
self.file.close() self.file.close()
self.file = None self.file = None
def __del__(self): def __del__(self):
self.close() if not self.file is None:
self.close()
def read_locked(filename): def read_locked(filename):
fp = LockedFile(filename, 'r') fp = LockedFile(filename, 'r')
data = fp.read() data = fp.read()
+129 -183
View File
@@ -33,12 +33,29 @@ exists = os.path.exists
pjoin = os.path.join pjoin = os.path.join
logger = logging.getLogger('web2py.rewrite') logger = logging.getLogger('web2py.rewrite')
thread = threading.local() # thread-local storage for routing params THREAD_LOCAL = threading.local() # thread-local storage for routing params
regex_at = re.compile(r'(?<!\\)\$[a-zA-Z]\w*') regex_at = re.compile(r'(?<!\\)\$[a-zA-Z]\w*')
regex_anything = re.compile(r'(?<!\\)\$anything') regex_anything = re.compile(r'(?<!\\)\$anything')
regex_redirect = re.compile(r'(\d+)->(.*)') regex_redirect = re.compile(r'(\d+)->(.*)')
regex_full_url = re.compile(r'^(?P<scheme>http|https|HTTP|HTTPS)\://(?P<host>[^/]*)(?P<uri>.*)') regex_full_url = re.compile(r'^(?P<scheme>http|https|HTTP|HTTPS)\://(?P<host>[^/]*)(?P<uri>.*)')
regex_version = re.compile(r'^(_[\d]+\.[\d]+\.[\d]+)$')
# pattern to replace spaces with underscore in URL
# also the html escaped variants '+' and '%20' are covered
regex_space = re.compile('(\+|\s|%20)+')
# pattern to find valid paths in url /application/controller/...
# this could be:
# for static pages:
# /<b:application>/static/<x:file>
# for dynamic pages:
# /<a:application>[/<c:controller>[/<f:function>[.<e:ext>][/<s:args>]]]
# application, controller, function and ext may only contain [a-zA-Z0-9_]
# file and args may also contain '-', '=', '.' and '/'
# apps in routes_apps_raw must parse raw_args into args
regex_url = re.compile('^/((?P<a>\w+)(/(?P<c>\w+)(/(?P<z>(?P<f>\w+)(\.(?P<e>[\w.]+))?(?P<s>[/\w@=-]*(\.[/\w@=-]+)*)))?)?)?$')
def _router_default(): def _router_default():
"return new copy of default base router" "return new copy of default base router"
@@ -84,7 +101,7 @@ def _params_default(app=None):
params_apps = dict() params_apps = dict()
params = _params_default(app=None) # regex rewrite parameters params = _params_default(app=None) # regex rewrite parameters
thread.routes = params # default to base regex rewrite parameters THREAD_LOCAL.routes = params # default to base regex rewrite parameters
routers = None routers = None
def log_rewrite(string): def log_rewrite(string):
@@ -107,16 +124,16 @@ def log_rewrite(string):
logger.debug(string) logger.debug(string)
ROUTER_KEYS = set( ROUTER_KEYS = set(
('default_application', 'applications', ('default_application', 'applications',
'default_controller', 'controllers', 'default_controller', 'controllers',
'default_function', 'functions', 'default_function', 'functions',
'default_language', 'languages', 'default_language', 'languages',
'domain', 'domains', 'root_static', 'path_prefix', 'domain', 'domains', 'root_static', 'path_prefix',
'exclusive_domain', 'map_hyphen', 'map_static', 'exclusive_domain', 'map_hyphen', 'map_static',
'acfe_match', 'file_match', 'args_match')) 'acfe_match', 'file_match', 'args_match'))
ROUTER_BASE_KEYS = set( ROUTER_BASE_KEYS = set(
('applications', 'default_application', ('applications', 'default_application',
'domains', 'path_prefix')) 'domains', 'path_prefix'))
# The external interface to rewrite consists of: # The external interface to rewrite consists of:
@@ -125,11 +142,11 @@ ROUTER_BASE_KEYS = set(
# url_in: parse and rewrite incoming URL # url_in: parse and rewrite incoming URL
# url_out: assemble and rewrite outgoing URL # url_out: assemble and rewrite outgoing URL
# #
# thread.routes.default_application # THREAD_LOCAL.routes.default_application
# thread.routes.error_message # THREAD_LOCAL.routes.error_message
# thread.routes.error_message_ticket # THREAD_LOCAL.routes.error_message_ticket
# thread.routes.try_redirect_on_error # THREAD_LOCAL.routes.try_redirect_on_error
# thread.routes.error_handler # THREAD_LOCAL.routes.error_handler
# #
# filter_url: helper for doctest & unittest # filter_url: helper for doctest & unittest
# filter_err: helper for doctest & unittest # filter_err: helper for doctest & unittest
@@ -140,8 +157,8 @@ def fixup_missing_path_info(environ):
path_info = eget('PATH_INFO') path_info = eget('PATH_INFO')
request_uri = eget('REQUEST_URI') request_uri = eget('REQUEST_URI')
if not path_info and request_uri: if not path_info and request_uri:
# for fcgi, get path_info and # for fcgi, get path_info and
# query_string from request_uri # query_string from request_uri
items = request_uri.split('?') items = request_uri.split('?')
path_info = environ['PATH_INFO'] = items[0] path_info = environ['PATH_INFO'] = items[0]
environ['QUERY_STRING'] = items[1] if len(items) > 1 else '' environ['QUERY_STRING'] = items[1] if len(items) > 1 else ''
@@ -154,45 +171,36 @@ def fixup_missing_path_info(environ):
if not eget('HTTP_HOST'): if not eget('HTTP_HOST'):
environ['HTTP_HOST'] = \ environ['HTTP_HOST'] = \
'%s:%s' % (eget('SERVER_NAME'),eget('SERVER_PORT')) '%s:%s' % (eget('SERVER_NAME'),eget('SERVER_PORT'))
def url_in(request, environ): def url_in(request, environ):
"parse and rewrite incoming URL" "parse and rewrite incoming URL"
if routers: if routers:
return map_url_in(request, environ) return map_url_in(request, environ)
return regex_url_in(request, environ) return regex_url_in(request, environ)
def url_out(request, env, application, controller, function, def url_out(request, environ, application, controller, function,
args, other, scheme, host, port): args, other, scheme, host, port):
"assemble and rewrite outgoing URL" "assemble and rewrite outgoing URL"
if routers: if routers:
acf = map_url_out(request, env, application, controller, acf = map_url_out(request, environ, application, controller,
function, args, other, scheme, host, port) function, args, other, scheme, host, port)
url = '%s%s' % (acf, other) url = '%s%s' % (acf, other)
else: else:
url = '/%s/%s/%s%s' % (application, controller, function, other) url = '/%s/%s/%s%s' % (application, controller, function, other)
url = regex_filter_out(url, env) url = regex_filter_out(url, environ)
# #
# fill in scheme and host if absolute URL is requested # fill in scheme and host if absolute URL is requested
# scheme can be a string, eg 'http', 'https', 'ws', 'wss' # scheme can be a string, eg 'http', 'https', 'ws', 'wss'
# #
if scheme or port is not None: if host is True or (host is None and (scheme or port!=None)):
if host is None: # scheme or port implies host host = request.env.http_host
host = True
if not scheme or scheme is True: if not scheme or scheme is True:
if request and request.env: scheme = request.env.get('wsgi_url_scheme', 'http').lower() \
scheme = request.env.get('wsgi_url_scheme', 'http').lower() if request else 'http'
else:
scheme = 'http' # some reasonable default in case we need it
if host is not None:
if host is True:
host = request.env.http_host
if host: if host:
if port is None: host_port = host if not port else host.split(':',1)[0]+':%s'%port
port = '' url = '%s://%s%s' % (scheme, host_port, url)
else:
port = ':%s' % port
url = '%s://%s%s%s' % (scheme, host, port, url)
return url return url
def try_rewrite_on_error(http_response, request, environ, ticket=None): def try_rewrite_on_error(http_response, request, environ, ticket=None):
@@ -200,12 +208,12 @@ def try_rewrite_on_error(http_response, request, environ, ticket=None):
called from main.wsgibase to rewrite the http response. called from main.wsgibase to rewrite the http response.
""" """
status = int(str(http_response.status).split()[0]) status = int(str(http_response.status).split()[0])
if status>=399 and thread.routes.routes_onerror: if status>=399 and THREAD_LOCAL.routes.routes_onerror:
keys=set(('%s/%s' % (request.application, status), keys=set(('%s/%s' % (request.application, status),
'%s/*' % (request.application), '%s/*' % (request.application),
'*/%s' % (status), '*/%s' % (status),
'*/*')) '*/*'))
for (key,uri) in thread.routes.routes_onerror: for (key,uri) in THREAD_LOCAL.routes.routes_onerror:
if key in keys: if key in keys:
if uri == '!': if uri == '!':
# do nothing! # do nothing!
@@ -229,7 +237,7 @@ def try_rewrite_on_error(http_response, request, environ, ticket=None):
path_info = '/' + path_info.lstrip('/') # add leading '/' if missing path_info = '/' + path_info.lstrip('/') # add leading '/' if missing
environ['PATH_INFO'] = path_info environ['PATH_INFO'] = path_info
error_handling_path = \ error_handling_path = \
url_in(request, environ)[1]['PATH_INFO'] url_in(request, environ)[2]['PATH_INFO']
# Avoid infinite loop. # Avoid infinite loop.
if error_handling_path != error_raising_path: if error_handling_path != error_raising_path:
# wsgibase will be called recursively with the routes_onerror path. # wsgibase will be called recursively with the routes_onerror path.
@@ -243,12 +251,12 @@ def try_rewrite_on_error(http_response, request, environ, ticket=None):
def try_redirect_on_error(http_object, request, ticket=None): def try_redirect_on_error(http_object, request, ticket=None):
"called from main.wsgibase to rewrite the http response" "called from main.wsgibase to rewrite the http response"
status = int(str(http_object.status).split()[0]) status = int(str(http_object.status).split()[0])
if status>399 and thread.routes.routes_onerror: if status>399 and THREAD_LOCAL.routes.routes_onerror:
keys=set(('%s/%s' % (request.application, status), keys=set(('%s/%s' % (request.application, status),
'%s/*' % (request.application), '%s/*' % (request.application),
'*/%s' % (status), '*/%s' % (status),
'*/*')) '*/*'))
for (key,redir) in thread.routes.routes_onerror: for (key,redir) in THREAD_LOCAL.routes.routes_onerror:
if key in keys: if key in keys:
if redir == '!': if redir == '!':
break break
@@ -281,7 +289,7 @@ def load(routes='routes.py', app=None, data=None, rdict=None):
global params_apps global params_apps
params_apps = dict() params_apps = dict()
params = _params_default(app=None) # regex rewrite parameters params = _params_default(app=None) # regex rewrite parameters
thread.routes = params # default to base regex rewrite parameters THREAD_LOCAL.routes = params # default to base regex rewrite parameters
routers = None routers = None
if isinstance(rdict, dict): if isinstance(rdict, dict):
@@ -328,7 +336,7 @@ def load(routes='routes.py', app=None, data=None, rdict=None):
if app is None: if app is None:
params = p # install base rewrite parameters params = p # install base rewrite parameters
thread.routes = params # install default as current routes THREAD_LOCAL.routes = params # install default as current routes
# #
# create the BASE router if routers in use # create the BASE router if routers in use
# #
@@ -537,24 +545,25 @@ def regex_select(env=None, app=None, request=None):
select a set of regex rewrite params for the current request select a set of regex rewrite params for the current request
""" """
if app: if app:
thread.routes = params_apps.get(app, params) THREAD_LOCAL.routes = params_apps.get(app, params)
elif env and params.routes_app: elif env and params.routes_app:
if routers: if routers:
map_url_in(request, env, app=True) map_url_in(request, env, app=True)
else: else:
app = regex_uri(env, params.routes_app, "routes_app") app = regex_uri(env, params.routes_app, "routes_app")
thread.routes = params_apps.get(app, params) THREAD_LOCAL.routes = params_apps.get(app, params)
else: else:
thread.routes = params # default to base rewrite parameters THREAD_LOCAL.routes = params # default to base rewrite parameters
log_rewrite("select routing parameters: %s" % thread.routes.name) log_rewrite("select routing parameters: %s" % THREAD_LOCAL.routes.name)
return app # for doctest return app # for doctest
def regex_filter_in(e): def regex_filter_in(e):
"regex rewrite incoming URL" "regex rewrite incoming URL"
routes = THREAD_LOCAL.routes
query = e.get('QUERY_STRING', None) query = e.get('QUERY_STRING', None)
e['WEB2PY_ORIGINAL_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '') e['WEB2PY_ORIGINAL_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '')
if thread.routes.routes_in: if routes.routes_in:
path = regex_uri(e, thread.routes.routes_in, path = regex_uri(e, routes.routes_in,
"routes_in", e['PATH_INFO']) "routes_in", e['PATH_INFO'])
rmatch = regex_redirect.match(path) rmatch = regex_redirect.match(path)
if rmatch: if rmatch:
@@ -570,58 +579,7 @@ def regex_filter_in(e):
e['REQUEST_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '') e['REQUEST_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '')
return e return e
# pattern to replace spaces with underscore in URL
# also the html escaped variants '+' and '%20' are covered
regex_space = re.compile('(\+|\s|%20)+')
# pattern to find valid paths in url /application/controller/...
# this could be:
# for static pages:
# /<b:application>/static/<x:file>
# for dynamic pages:
# /<a:application>[/<c:controller>[/<f:function>[.<e:ext>][/<s:args>]]]
# application, controller, function and ext may only contain [a-zA-Z0-9_]
# file and args may also contain '-', '=', '.' and '/'
# apps in routes_apps_raw must parse raw_args into args
regex_static = re.compile(r'''
(^ # static pages
/(?P<b> \w+) # b=app
/static # /b/static
/(?P<x> (\w[\-\=\./]?)* ) # x=file
$)
''', re.X)
regex_url = re.compile(r'''
(^( # (/a/c/f.e/s)
/(?P<a> [\w\s+]+ ) # /a=app
( # (/c.f.e/s)
/(?P<c> [\w\s+]+ ) # /a/c=controller
( # (/f.e/s)
/(?P<f> [\w\s+]+ ) # /a/c/f=function
( # (.e)
\.(?P<e> [\w\s+]+ ) # /a/c/f.e=extension
)?
( # (/s)
/(?P<r> # /a/c/f.e/r=raw_args
.*
)
)?
)?
)?
)?
/?$)
''', re.X)
regex_args = re.compile(r'''
(^
(?P<s>
( [\w@/-][=.]? )* # s=args
)?
/?$) # trailing slash
''', re.X)
def sluggify(key): def sluggify(key):
return key.lower().replace('.','_') return key.lower().replace('.','_')
@@ -635,72 +593,61 @@ def regex_url_in(request, environ):
# ################################################## # ##################################################
regex_select(env=environ, request=request) regex_select(env=environ, request=request)
routes = THREAD_LOCAL.routes
if thread.routes.routes_in: if routes.routes_in:
environ = regex_filter_in(environ) environ = regex_filter_in(environ)
request.env.update((sluggify(k),v) for k,v in environ.iteritems()) request.env.update((sluggify(k),v) for k,v in environ.iteritems())
path = request.env.path_info.replace('\\', '/')
# ################################################## # ##################################################
# serve if a static file # serve if a static file
# ################################################## # ##################################################
match = regex_static.match(regex_space.sub('_', path)) path = request.env.path_info.replace('\\', '/') or '/'
if match and match.group('x'): path = regex_space.sub('_',path)
static_file = pjoin(request.env.applications_parent, if path.endswith('/') and len(path)>1: path = path[:-1]
'applications', match.group('b'),
'static', match.group('x'))
return (static_file, environ)
# ##################################################
# parse application, controller and function
# ##################################################
path = re.sub('%20', ' ', path)
match = regex_url.match(path) match = regex_url.match(path)
if not match or match.group('c') == 'static': if not match:
raise HTTP(400, raise HTTP(400,
thread.routes.error_message % 'invalid request', routes.error_message % 'invalid request',
web2py_error='invalid path') web2py_error='invalid path')
elif match.group('c')=='static':
request.application = \ application = match.group('a')
regex_space.sub('_', match.group('a') or thread.routes.default_application) version, filename = None, match.group('z')
request.controller = \ items = filename.split('/',1)
regex_space.sub('_', match.group('c') or thread.routes.default_controller) if regex_version.match(items[0]):
request.function = \ version,filename = items
regex_space.sub('_', match.group('f') or thread.routes.default_function) static_file = pjoin(request.env.applications_parent,
group_e = match.group('e') 'applications', application,
request.raw_extension = group_e and regex_space.sub('_', group_e) or None 'static', filename)
request.extension = request.raw_extension or 'html' return (static_file, version, environ)
request.raw_args = match.group('r') else:
request.args = List([]) # ##################################################
if request.application in thread.routes.routes_apps_raw: # parse application, controller and function
# application is responsible for parsing args # ##################################################
request.args = None request.application = match.group('a') or routes.default_application
elif request.raw_args: request.controller = match.group('c') or routes.default_controller
match = regex_args.match(request.raw_args.replace(' ', '_')) request.function = match.group('f') or routes.default_function
if match: request.raw_extension = match.group('e')
group_s = match.group('s') request.extension = request.raw_extension or 'html'
request.args = \ request.raw_args = match.group('s')
List((group_s and group_s.split('/')) or []) if request.application in routes.routes_apps_raw:
if request.args and request.args[-1] == '': # application is responsible for parsing args
request.args.pop() # adjust for trailing empty arg request.args = None
elif request.raw_args:
request.args = List(request.raw_args.split('/')[1:])
else: else:
raise HTTP(400, request.args = List([])
thread.routes.error_message % 'invalid request', return (None, None, environ)
web2py_error='invalid path (args)')
return (None, environ)
def regex_filter_out(url, e=None): def regex_filter_out(url, e=None):
"regex rewrite outgoing URL" "regex rewrite outgoing URL"
if not hasattr(thread, 'routes'): if not hasattr(THREAD_LOCAL, 'routes'):
regex_select() # ensure thread.routes is set (for application threads) regex_select() # ensure routes is set (for application threads)
routes = THREAD_LOCAL.routes
if routers: if routers:
return url # already filtered return url # already filtered
if thread.routes.routes_out: if routes.routes_out:
items = url.split('?', 1) items = url.split('?', 1)
if e: if e:
host = e.get('http_host', 'localhost').lower() host = e.get('http_host', 'localhost').lower()
@@ -713,7 +660,7 @@ def regex_filter_out(url, e=None):
e.get('request_method', 'get').lower(), items[0]) e.get('request_method', 'get').lower(), items[0])
else: else:
items[0] = ':http://localhost:get %s' % items[0] items[0] = ':http://localhost:get %s' % items[0]
for (regex, value, tmp) in thread.routes.routes_out: for (regex, value, tmp) in routes.routes_out:
if regex.match(items[0]): if regex.match(items[0]):
rewritten = '?'.join([regex.sub(value, items[0])] + items[1:]) rewritten = '?'.join([regex.sub(value, items[0])] + items[1:])
log_rewrite('routes_out: [%s] -> %s' % (url, rewritten)) log_rewrite('routes_out: [%s] -> %s' % (url, rewritten))
@@ -722,9 +669,9 @@ def regex_filter_out(url, e=None):
return url return url
def filter_url(url, method='get', remote='0.0.0.0', def filter_url(url, method='get', remote='0.0.0.0',
out=False, app=False, lang=None, out=False, app=False, lang=None,
domain=(None,None), env=False, scheme=None, domain=(None,None), env=False, scheme=None,
host=None, port=None): host=None, port=None):
""" """
doctest/unittest interface to regex_filter_in() and regex_filter_out() doctest/unittest interface to regex_filter_in() and regex_filter_out()
@@ -793,7 +740,7 @@ def filter_url(url, method='get', remote='0.0.0.0',
# rewrite inbound URL # rewrite inbound URL
# #
(static, e) = url_in(request, e) (static, version, e) = url_in(request, e)
if static: if static:
return static return static
result = "/%s/%s/%s" % (request.application, request.controller, request.function) result = "/%s/%s/%s" % (request.application, request.controller, request.function)
@@ -812,12 +759,13 @@ def filter_url(url, method='get', remote='0.0.0.0',
def filter_err(status, application='app', ticket='tkt'): def filter_err(status, application='app', ticket='tkt'):
"doctest/unittest interface to routes_onerror" "doctest/unittest interface to routes_onerror"
if status > 399 and thread.routes.routes_onerror: routes = THREAD_LOCAL.routes
if status > 399 and routes.routes_onerror:
keys = set(('%s/%s' % (application, status), keys = set(('%s/%s' % (application, status),
'%s/*' % (application), '%s/*' % (application),
'*/%s' % (status), '*/%s' % (status),
'*/*')) '*/*'))
for (key,redir) in thread.routes.routes_onerror: for (key,redir) in routes.routes_onerror:
if key in keys: if key in keys:
if redir == '!': if redir == '!':
break break
@@ -921,12 +869,12 @@ class MapUrlIn(object):
self.pop_arg_if(self.application == arg0) self.pop_arg_if(self.application == arg0)
if not base._acfe_match.match(self.application): if not base._acfe_match.match(self.application):
raise HTTP(400, thread.routes.error_message % 'invalid request', raise HTTP(400, THREAD_LOCAL.routes.error_message % 'invalid request',
web2py_error="invalid application: '%s'" % self.application) web2py_error="invalid application: '%s'" % self.application)
if self.application not in routers and \ if self.application not in routers and \
(self.application != thread.routes.default_application or self.application == 'welcome'): (self.application != THREAD_LOCAL.routes.default_application or self.application == 'welcome'):
raise HTTP(400, thread.routes.error_message % 'invalid request', raise HTTP(400, THREAD_LOCAL.routes.error_message % 'invalid request',
web2py_error="unknown application: '%s'" % self.application) web2py_error="unknown application: '%s'" % self.application)
# set the application router # set the application router
@@ -956,14 +904,15 @@ class MapUrlIn(object):
a root-static file is one whose incoming URL expects it to be at the root, a root-static file is one whose incoming URL expects it to be at the root,
typically robots.txt & favicon.ico typically robots.txt & favicon.ico
''' '''
if len(self.args) == 1 and self.arg0 in self.router.root_static: if len(self.args) == 1 and self.arg0 in self.router.root_static:
self.controller = self.request.controller = 'static' self.controller = self.request.controller = 'static'
root_static_file = pjoin(self.request.env.applications_parent, root_static_file = pjoin(self.request.env.applications_parent,
'applications', self.application, 'applications', self.application,
self.controller, self.arg0) self.controller, self.arg0)
log_rewrite("route: root static=%s" % root_static_file) log_rewrite("route: root static=%s" % root_static_file)
return root_static_file return root_static_file, None
return None return None, None
def map_language(self): def map_language(self):
"handle language (no hyphen mapping)" "handle language (no hyphen mapping)"
@@ -989,7 +938,7 @@ class MapUrlIn(object):
self.pop_arg_if(arg0 == self.controller) self.pop_arg_if(arg0 == self.controller)
log_rewrite("route: controller=%s" % self.controller) log_rewrite("route: controller=%s" % self.controller)
if not self.router._acfe_match.match(self.controller): if not self.router._acfe_match.match(self.controller):
raise HTTP(400, thread.routes.error_message % 'invalid request', raise HTTP(400, THREAD_LOCAL.routes.error_message % 'invalid request',
web2py_error='invalid controller') web2py_error='invalid controller')
def map_static(self): def map_static(self):
@@ -998,8 +947,12 @@ class MapUrlIn(object):
file_match but no hyphen mapping file_match but no hyphen mapping
''' '''
if self.controller != 'static': if self.controller != 'static':
return None return None, None
file = '/'.join(self.args) version = regex_version.match(self.args(0))
if self.args and version:
file = '/'.join(self.args[1:])
else:
file = '/'.join(self.args)
if len(self.args) == 0: if len(self.args) == 0:
bad_static = True # require a file name bad_static = True # require a file name
elif '/' in self.file_match: elif '/' in self.file_match:
@@ -1013,7 +966,7 @@ class MapUrlIn(object):
if bad_static: if bad_static:
log_rewrite('bad static path=%s' % file) log_rewrite('bad static path=%s' % file)
raise HTTP(400, raise HTTP(400,
thread.routes.error_message % 'invalid request', THREAD_LOCAL.routes.error_message % 'invalid request',
web2py_error='invalid static file') web2py_error='invalid static file')
# #
# support language-specific static subdirectories, # support language-specific static subdirectories,
@@ -1030,7 +983,7 @@ class MapUrlIn(object):
'static', file) 'static', file)
self.extension = None self.extension = None
log_rewrite("route: static=%s" % static_file) log_rewrite("route: static=%s" % static_file)
return static_file return static_file, version
def map_function(self): def map_function(self):
"handle function.extension" "handle function.extension"
@@ -1055,10 +1008,10 @@ class MapUrlIn(object):
log_rewrite("route: function.ext=%s.%s" % (self.function, self.extension)) log_rewrite("route: function.ext=%s.%s" % (self.function, self.extension))
if not self.router._acfe_match.match(self.function): if not self.router._acfe_match.match(self.function):
raise HTTP(400, thread.routes.error_message % 'invalid request', raise HTTP(400, THREAD_LOCAL.routes.error_message % 'invalid request',
web2py_error='invalid function') web2py_error='invalid function')
if self.extension and not self.router._acfe_match.match(self.extension): if self.extension and not self.router._acfe_match.match(self.extension):
raise HTTP(400, thread.routes.error_message % 'invalid request', raise HTTP(400, THREAD_LOCAL.routes.error_message % 'invalid request',
web2py_error='invalid extension') web2py_error='invalid extension')
def validate_args(self): def validate_args(self):
@@ -1067,14 +1020,14 @@ class MapUrlIn(object):
''' '''
for arg in self.args: for arg in self.args:
if not self.router._args_match.match(arg): if not self.router._args_match.match(arg):
raise HTTP(400, thread.routes.error_message % 'invalid request', raise HTTP(400, THREAD_LOCAL.routes.error_message % 'invalid request',
web2py_error='invalid arg <%s>' % arg) web2py_error='invalid arg <%s>' % arg)
def sluggify(self): def sluggify(self):
"" ""
self.request.env.update( self.request.env.update(
(sluggify(k),v) for k,v in self.env.iteritems()) (sluggify(k),v) for k,v in self.env.iteritems())
def update_request(self): def update_request(self):
''' '''
update request from self update request from self
@@ -1098,7 +1051,7 @@ class MapUrlIn(object):
if self.language: if self.language:
uri = '/%s%s' % (self.language, uri) uri = '/%s%s' % (self.language, uri)
uri = '/%s%s%s%s' % ( uri = '/%s%s%s%s' % (
app, app,
uri, uri,
urllib.quote('/'+'/'.join(str(x) for x in self.args)) if self.args else '', urllib.quote('/'+'/'.join(str(x) for x in self.args)) if self.args else '',
('?' + self.query) if self.query else '') ('?' + self.query) if self.query else '')
@@ -1292,24 +1245,24 @@ def map_url_in(request, env, app=False):
# initialize router-url object # initialize router-url object
# #
thread.routes = params # default to base routes THREAD_LOCAL.routes = params # default to base routes
map = MapUrlIn(request=request, env=env) map = MapUrlIn(request=request, env=env)
map.sluggify() map.sluggify()
map.map_prefix() # strip prefix if present map.map_prefix() # strip prefix if present
map.map_app() # determine application map.map_app() # determine application
# configure thread.routes for error rewrite # configure THREAD_LOCAL.routes for error rewrite
# #
if params.routes_app: if params.routes_app:
thread.routes = params_apps.get(app, params) THREAD_LOCAL.routes = params_apps.get(app, params)
if app: if app:
return map.application return map.application
root_static_file = map.map_root_static() # handle root-static files root_static_file, version = map.map_root_static() # handle root-static files
if root_static_file: if root_static_file:
map.update_request() map.update_request()
return (root_static_file, map.env) return (root_static_file, version, map.env)
# handle mapping of lang/static to static/lang in externally-rewritten URLs # handle mapping of lang/static to static/lang in externally-rewritten URLs
# in case we have to handle them ourselves # in case we have to handle them ourselves
if map.languages and map.map_static is False and map.arg0 == 'static' and map.args(1) in map.languages: if map.languages and map.map_static is False and map.arg0 == 'static' and map.args(1) in map.languages:
@@ -1318,14 +1271,14 @@ def map_url_in(request, env, app=False):
else: else:
map.map_language() map.map_language()
map.map_controller() map.map_controller()
static_file = map.map_static() static_file, version = map.map_static()
if static_file: if static_file:
map.update_request() map.update_request()
return (static_file, map.env) return (static_file, version, map.env)
map.map_function() map.map_function()
map.validate_args() map.validate_args()
map.update_request() map.update_request()
return (None, map.env) return (None, None, map.env)
def map_url_out(request, env, application, controller, def map_url_out(request, env, application, controller,
function, args, other, scheme, host, port): function, args, other, scheme, host, port):
@@ -1363,10 +1316,3 @@ def get_effective_router(appname):
if not routers or appname not in routers: if not routers or appname not in routers:
return None return None
return Storage(routers[appname]) # return a copy return Storage(routers[appname]) # return a copy
+16 -222
View File
@@ -2,6 +2,7 @@
# This file is part of the Rocket Web Server # This file is part of the Rocket Web Server
# Copyright (c) 2011 Timothy Farrell # Copyright (c) 2011 Timothy Farrell
# Modified by Massimo Di Pierro
# Import System Modules # Import System Modules
import sys import sys
@@ -12,7 +13,7 @@ import platform
import traceback import traceback
# Define Constants # Define Constants
VERSION = '1.2.4' VERSION = '1.2.5'
SERVER_NAME = socket.gethostname() SERVER_NAME = socket.gethostname()
SERVER_SOFTWARE = 'Rocket %s' % VERSION SERVER_SOFTWARE = 'Rocket %s' % VERSION
HTTP_SERVER_SOFTWARE = '%s Python/%s' % (SERVER_SOFTWARE, sys.version.split(' ')[0]) HTTP_SERVER_SOFTWARE = '%s Python/%s' % (SERVER_SOFTWARE, sys.version.split(' ')[0])
@@ -1454,41 +1455,34 @@ class Worker(Thread):
return req return req
def read_headers(self, sock_file): def read_headers(self, sock_file, environ):
try: try:
headers = dict()
l = sock_file.readline()
lname = None lname = None
lval = None
while True: while True:
l = sock_file.readline()
if PY3K: if PY3K:
try: try:
l = str(l, 'ISO-8859-1') l = str(l, 'ISO-8859-1')
except UnicodeDecodeError: except UnicodeDecodeError:
self.err_log.warning('Client sent invalid header: ' + repr(l)) self.err_log.warning('Invalid request header: '+repr(l))
if l == '\r\n': if l.strip().replace('\0','') == '':
break break
elif l[0] in ' \t' and lname:
if l[0] in ' \t' and lname:
# Some headers take more than one line # Some headers take more than one line
lval += ',' + l.strip() environ[lname] += ' ' + l.strip()
else: else:
# HTTP header values are latin-1 encoded # HTTP header values are latin-1 encoded
l = l.split(':', 1) l = l.split(':', 1)
# HTTP header names are us-ascii encoded # HTTP header names are us-ascii encoded
lname = l[0].strip().upper().replace('-', '_') lname = str('HTTP_'+l[0].strip().upper().replace('-', '_'))
lval = l[-1].strip() lval = str(l[-1].strip())
headers[str(lname)] = str(lval) environ[lname] = lval
l = sock_file.readline()
except socket.timeout: except socket.timeout:
raise SocketTimeout("Socket timed out before request.") raise SocketTimeout("Socket timed out before request.")
return headers
class SocketTimeout(Exception): class SocketTimeout(Exception):
"Exception for when a socket times out between requests." "Exception for when a socket times out between requests."
pass pass
@@ -1549,209 +1543,10 @@ class ChunkedReader(object):
yield self.readline() yield self.readline()
def get_method(method): def get_method(method):
methods = dict(wsgi=WSGIWorker)
methods = dict(wsgi=WSGIWorker,
fs=FileSystemWorker)
return methods[method.lower()] return methods[method.lower()]
# Monolithic build...end of module: rocket\worker.py # Monolithic build...end of module: rocket\worker.py
# Monolithic build...start of module: rocket\methods\__init__.py
# Monolithic build...end of module: rocket\methods\__init__.py
# Monolithic build...start of module: rocket\methods\fs.py
# Import System Modules
import os
import time
import mimetypes
from email.utils import formatdate
from wsgiref.headers import Headers
from wsgiref.util import FileWrapper
# Import Package Modules
# package imports removed in monolithic build
# Define Constants
CHUNK_SIZE = 2**16 # 64 Kilobyte chunks
HEADER_RESPONSE = '''HTTP/1.1 %s\r\n%s'''
INDEX_HEADER = '''\
<html>
<head><title>Directory Index: %(path)s</title>
<style> .parent { margin-bottom: 1em; }</style>
</head>
<body><h1>Directory Index: %(path)s</h1>
<table>
<tr><th>Directories</th></tr>
'''
INDEX_ROW = '''<tr><td><div class="%(cls)s"><a href="/%(link)s">%(name)s</a></div></td></tr>'''
INDEX_FOOTER = '''</table></body></html>\r\n'''
class LimitingFileWrapper(FileWrapper):
def __init__(self, limit=None, *args, **kwargs):
self.limit = limit
FileWrapper.__init__(self, *args, **kwargs)
def read(self, amt):
if amt > self.limit:
amt = self.limit
self.limit -= amt
return FileWrapper.read(self, amt)
class FileSystemWorker(Worker):
def __init__(self, *args, **kwargs):
"""Builds some instance variables that will last the life of the
thread."""
Worker.__init__(self, *args, **kwargs)
self.root = os.path.abspath(self.app_info['document_root'])
self.display_index = self.app_info['display_index']
def serve_file(self, filepath, headers):
filestat = os.stat(filepath)
self.size = filestat.st_size
modtime = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
time.gmtime(filestat.st_mtime))
self.headers.add_header('Last-Modified', modtime)
if headers.get('if_modified_since') == modtime:
# The browser cache is up-to-date, send a 304.
self.status = "304 Not Modified"
self.data = []
return
ct = mimetypes.guess_type(filepath)[0]
self.content_type = ct if ct else 'text/plain'
try:
f = open(filepath, 'rb')
self.headers['Pragma'] = 'cache'
self.headers['Cache-Control'] = 'private'
self.headers['Content-Length'] = str(self.size)
if self.etag:
self.headers.add_header('Etag', self.etag)
if self.expires:
self.headers.add_header('Expires', self.expires)
try:
# Implement 206 partial file support.
start, end = headers['range'].split('-')
start = 0 if not start.isdigit() else int(start)
end = self.size if not end.isdigit() else int(end)
if self.size < end or start < 0:
self.status = "214 Unsatisfiable Range Requested"
self.data = FileWrapper(f, CHUNK_SIZE)
else:
f.seek(start)
self.data = LimitingFileWrapper(f, CHUNK_SIZE, limit=end)
self.status = "206 Partial Content"
except:
self.data = FileWrapper(f, CHUNK_SIZE)
except IOError:
self.status = "403 Forbidden"
def serve_dir(self, pth, rpth):
def rel_path(path):
return os.path.normpath(path[len(self.root):] if path.startswith(self.root) else path)
if not self.display_index:
self.status = '404 File Not Found'
return b('')
else:
self.content_type = 'text/html'
dir_contents = [os.path.join(pth, x) for x in os.listdir(os.path.normpath(pth))]
dir_contents.sort()
dirs = [rel_path(x)+'/' for x in dir_contents if os.path.isdir(x)]
files = [rel_path(x) for x in dir_contents if os.path.isfile(x)]
self.data = [INDEX_HEADER % dict(path='/'+rpth)]
if rpth:
self.data += [INDEX_ROW % dict(name='(parent directory)', cls='dir parent', link='/'.join(rpth[:-1].split('/')[:-1]))]
self.data += [INDEX_ROW % dict(name=os.path.basename(x[:-1]), link=os.path.join(rpth, os.path.basename(x[:-1])).replace('\\', '/'), cls='dir') for x in dirs]
self.data += ['<tr><th>Files</th></tr>']
self.data += [INDEX_ROW % dict(name=os.path.basename(x), link=os.path.join(rpth, os.path.basename(x)).replace('\\', '/'), cls='file') for x in files]
self.data += [INDEX_FOOTER]
self.headers['Content-Length'] = self.size = str(sum([len(x) for x in self.data]))
self.status = '200 OK'
def run_app(self, conn):
self.status = "200 OK"
self.size = 0
self.expires = None
self.etag = None
self.content_type = 'text/plain'
self.content_length = None
if __debug__:
self.err_log.debug('Getting sock_file')
# Build our file-like object
sock_file = conn.makefile('rb',BUF_SIZE)
request = self.read_request_line(sock_file)
if request['method'].upper() not in ('GET', ):
self.status = "501 Not Implemented"
try:
# Get our file path
reader = self.read_headers(sock_file)
headers = dict((k.lower(),v) for k,v in reader.iteritems())
rpath = request.get('path', '').lstrip('/')
filepath = os.path.join(self.root, rpath)
filepath = os.path.abspath(filepath)
if __debug__:
self.err_log.debug('Request for path: %s' % filepath)
self.closeConnection = headers.get('connection', 'close').lower() == 'close'
self.headers = Headers([('Date', formatdate(usegmt=True)),
('Server', HTTP_SERVER_SOFTWARE),
('Connection', headers.get('connection', 'close')),
])
if not filepath.lower().startswith(self.root.lower()):
# File must be within our root directory
self.status = "400 Bad Request"
self.closeConnection = True
elif not os.path.exists(filepath):
self.status = "404 File Not Found"
self.closeConnection = True
elif os.path.isdir(filepath):
self.serve_dir(filepath, rpath)
elif os.path.isfile(filepath):
self.serve_file(filepath, headers)
else:
# It exists but it's not a file or a directory????
# What is it then?
self.status = "501 Not Implemented"
self.closeConnection = True
h = self.headers
statcode, statstr = self.status.split(' ', 1)
statcode = int(statcode)
if statcode >= 400:
h.add_header('Content-Type', self.content_type)
self.data = [statstr]
# Build our output headers
header_data = HEADER_RESPONSE % (self.status, str(h))
# Send the headers
if __debug__:
self.err_log.debug('Sending Headers: %s' % repr(header_data))
self.conn.sendall(b(header_data))
for data in self.data:
self.conn.sendall(b(data))
if hasattr(self.data, 'close'):
self.data.close()
finally:
if __debug__:
self.err_log.debug('Finally closing sock_file')
sock_file.close()
# Monolithic build...end of module: rocket\methods\fs.py
# Monolithic build...start of module: rocket\methods\wsgi.py # Monolithic build...start of module: rocket\methods\wsgi.py
# Import System Modules # Import System Modules
@@ -1819,16 +1614,15 @@ class WSGIWorker(Worker):
environ = self.base_environ.copy() environ = self.base_environ.copy()
# Grab the headers # Grab the headers
for k, v in self.read_headers(sock_file).items(): self.read_headers(sock_file,environ)
environ[str('HTTP_'+k)] = v
# Add CGI Variables # Add CGI Variables
environ['REQUEST_METHOD'] = request['method']
environ['PATH_INFO'] = request['path']
environ['SERVER_PROTOCOL'] = request['protocol']
environ['SERVER_PORT'] = str(conn.server_port) environ['SERVER_PORT'] = str(conn.server_port)
environ['REMOTE_PORT'] = str(conn.client_port) environ['REMOTE_PORT'] = str(conn.client_port)
environ['REMOTE_ADDR'] = str(conn.client_addr) environ['REMOTE_ADDR'] = str(conn.client_addr)
environ['REQUEST_METHOD'] = request['method']
environ['PATH_INFO'] = request['path']
environ['SERVER_PROTOCOL'] = request['protocol']
environ['QUERY_STRING'] = request['query_string'] environ['QUERY_STRING'] = request['query_string']
if 'HTTP_CONTENT_LENGTH' in environ: if 'HTTP_CONTENT_LENGTH' in environ:
environ['CONTENT_LENGTH'] = environ['HTTP_CONTENT_LENGTH'] environ['CONTENT_LENGTH'] = environ['HTTP_CONTENT_LENGTH']
+4 -3
View File
@@ -88,7 +88,7 @@ except:
from simplejson import loads, dumps from simplejson import loads, dumps
from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB, IS_INT_IN_RANGE from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB, IS_INT_IN_RANGE, IS_DATETIME
from gluon.utils import web2py_uuid from gluon.utils import web2py_uuid
@@ -443,7 +443,7 @@ class Scheduler(MetaScheduler):
Field('application_name',requires=IS_NOT_EMPTY(), Field('application_name',requires=IS_NOT_EMPTY(),
default=None,writable=False), default=None,writable=False),
Field('task_name',default=None), Field('task_name',default=None),
Field('group_name',default='main',writable=False), Field('group_name',default='main'),
Field('status',requires=IS_IN_SET(TASK_STATUS), Field('status',requires=IS_IN_SET(TASK_STATUS),
default=QUEUED,writable=False), default=QUEUED,writable=False),
Field('function_name', Field('function_name',
@@ -454,7 +454,8 @@ class Scheduler(MetaScheduler):
Field('args','text',default='[]',requires=TYPE(list)), Field('args','text',default='[]',requires=TYPE(list)),
Field('vars','text',default='{}',requires=TYPE(dict)), Field('vars','text',default='{}',requires=TYPE(dict)),
Field('enabled','boolean',default=True), Field('enabled','boolean',default=True),
Field('start_time','datetime',default=now, requires=IS_NOT_EMPTY()), Field('start_time','datetime',default=now,
requires = IS_DATETIME()),
Field('next_run_time','datetime',default=now), Field('next_run_time','datetime',default=now),
Field('stop_time','datetime'), Field('stop_time','datetime'),
Field('repeats','integer',default=1,comment="0=unlimited", Field('repeats','integer',default=1,comment="0=unlimited",
+1 -1
View File
@@ -6,6 +6,7 @@ License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
import os import os
import sys import sys
import socket
import platform import platform
from storage import Storage from storage import Storage
@@ -37,4 +38,3 @@ global_settings.is_jython = \
str(sys.copyright).find('Jython') > 0 str(sys.copyright).find('Jython') > 0
+2 -3
View File
@@ -19,7 +19,7 @@ import optparse
import glob import glob
import traceback import traceback
import fileutils import fileutils
import settings from settings import global_settings
from utils import web2py_uuid from utils import web2py_uuid
from compileapp import build_environment, read_pyc, run_models_in from compileapp import build_environment, read_pyc, run_models_in
from restricted import RestrictedError from restricted import RestrictedError
@@ -28,7 +28,6 @@ from storage import Storage
from admin import w2p_unpack from admin import w2p_unpack
from dal import BaseAdapter from dal import BaseAdapter
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
def exec_environment( def exec_environment(
@@ -112,7 +111,7 @@ def env(
request.env.path_info = '/%s/%s/%s' % (a, c, f) request.env.path_info = '/%s/%s/%s' % (a, c, f)
request.env.http_host = '127.0.0.1:8000' request.env.http_host = '127.0.0.1:8000'
request.env.remote_addr = '127.0.0.1' request.env.remote_addr = '127.0.0.1'
request.env.web2py_runtime_gae = settings.global_settings.web2py_runtime_gae request.env.web2py_runtime_gae = global_settings.web2py_runtime_gae
for k,v in extra_request.items(): for k,v in extra_request.items():
request[k] = v request[k] = v
+100 -56
View File
@@ -28,7 +28,7 @@ from dal import DAL, Field, Table, Row, CALLABLETYPES, smart_query, \
from storage import Storage from storage import Storage
from utils import md5_hash from utils import md5_hash
from validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \ from validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \
IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE, IS_STRONG
import datetime import datetime
import urllib import urllib
@@ -174,8 +174,7 @@ class TextWidget(FormWidget):
""" """
default = dict(value = value) default = dict(value = value)
attr = cls._attributes(field, default, attr = cls._attributes(field, default,**attributes)
**attributes)
return TEXTAREA(**attr) return TEXTAREA(**attr)
@@ -239,11 +238,14 @@ class ListWidget(StringWidget):
_name = field.name _name = field.name
if field.type=='list:integer': _class = 'integer' if field.type=='list:integer': _class = 'integer'
else: _class = 'string' else: _class = 'string'
requires = field.requires if isinstance(field.requires, (IS_NOT_EMPTY, IS_LIST_OF)) else None requires = field.requires if isinstance(
field.requires, (IS_NOT_EMPTY, IS_LIST_OF)) else None
attributes['_style'] = 'list-style:none' attributes['_style'] = 'list-style:none'
items=[LI(INPUT(_id=_id, _class=_class, _name=_name, nvalue = value or ['']
value=v, hideerror=True, requires=requires), items = [LI(INPUT(_id=_id, _class=_class, _name=_name,
**attributes) for v in value or ['']] value=v, hideerror=k<len(nvalue)-1,
requires=requires),
**attributes) for (k,v) in enumerate(nvalue)]
script=SCRIPT(""" script=SCRIPT("""
// from http://refactormycode.com/codes/694-expanding-input-list-using-jquery // from http://refactormycode.com/codes/694-expanding-input-list-using-jquery
(function(){ (function(){
@@ -377,12 +379,11 @@ class CheckboxesWidget(OptionsWidget):
requires = field.requires requires = field.requires
if not isinstance(requires, (list, tuple)): if not isinstance(requires, (list, tuple)):
requires = [requires] requires = [requires]
if requires: if requires and hasattr(requires[0], 'options'):
if hasattr(requires[0], 'options'): options = requires[0].options()
options = requires[0].options() else:
else: raise SyntaxError, 'widget cannot determine options of %s' \
raise SyntaxError, 'widget cannot determine options of %s' \ % field
% field
options = [(k, v) for k, v in options if k != ''] options = [(k, v) for k, v in options if k != '']
opts = [] opts = []
@@ -439,14 +440,23 @@ class PasswordWidget(FormWidget):
see also: :meth:`FormWidget.widget` see also: :meth:`FormWidget.widget`
""" """
# detect if attached a IS_STRONG with entropy
default=dict( default=dict(
_type='password', _type='password',
_value=(value and cls.DEFAULT_PASSWORD_DISPLAY) or '', _value=(value and cls.DEFAULT_PASSWORD_DISPLAY) or '',
) )
attr = cls._attributes(field, default, **attributes) attr = cls._attributes(field, default, **attributes)
output = CAT(INPUT(**attr))
return INPUT(**attr) # deal with entropy check!
requires = field.requires
if not isinstance(requires,(list,tuple)): requires = [requires]
is_strong = [r for r in requires if isinstance(r, IS_STRONG)]
if is_strong:
output.append(SCRIPT("web2py_validate_entropy(jQuery('#%s'),%s);" \
% (attr['_id'],is_strong[0].entropy)))
# end entropy check
return output
class UploadWidget(FormWidget): class UploadWidget(FormWidget):
@@ -679,6 +689,16 @@ def formstyle_divs(form, fields):
table.append(DIV(_label, _controls, _help, _id=id)) table.append(DIV(_label, _controls, _help, _id=id))
return table return table
def formstyle_inline(form, fields):
''' divs only '''
if len(fields)!=2:
raise RuntimeError, "Not possible"
id, label, controls, help = fields[0]
submit_button = fields[1][2]
return CAT(DIV(controls,_style='display:inline'),
submit_button)
def formstyle_ul(form, fields): def formstyle_ul(form, fields):
''' unordered list ''' ''' unordered list '''
table = UL() table = UL()
@@ -804,6 +824,7 @@ class SQLFORM(FORM):
divs = formstyle_divs, divs = formstyle_divs,
ul = formstyle_ul, ul = formstyle_ul,
bootstrap = formstyle_bootstrap, bootstrap = formstyle_bootstrap,
inline = formstyle_inline,
)) ))
FIELDNAME_REQUEST_DELETE = 'delete_this_record' FIELDNAME_REQUEST_DELETE = 'delete_this_record'
@@ -865,6 +886,8 @@ class SQLFORM(FORM):
self.ignore_rw = ignore_rw self.ignore_rw = ignore_rw
self.formstyle = formstyle self.formstyle = formstyle
self.readonly = readonly self.readonly = readonly
# Default dbio setting
self.detect_record_change = None
nbsp = XML('&nbsp;') # Firefox2 does not display fields with blanks nbsp = XML('&nbsp;') # Firefox2 does not display fields with blanks
FORM.__init__(self, *[], **attributes) FORM.__init__(self, *[], **attributes)
@@ -998,11 +1021,11 @@ class SQLFORM(FORM):
else: else:
inp = field.formatter(default) inp = field.formatter(default)
elif field.type == 'upload': elif field.type == 'upload':
if hasattr(field, 'widget') and field.widget: if field.widget:
inp = field.widget(field, default, upload) inp = field.widget(field, default, upload)
else: else:
inp = self.widgets.upload.widget(field, default, upload) inp = self.widgets.upload.widget(field, default, upload)
elif hasattr(field, 'widget') and field.widget: elif field.widget:
inp = field.widget(field, default) inp = field.widget(field, default)
elif field.type == 'boolean': elif field.type == 'boolean':
inp = self.widgets.boolean.widget(field, default) inp = self.widgets.boolean.widget(field, default)
@@ -1036,7 +1059,7 @@ class SQLFORM(FORM):
xfields.append((row_id,label,inp,comment)) xfields.append((row_id,label,inp,comment))
self.custom.dspval[fieldname] = dspval or nbsp self.custom.dspval[fieldname] = dspval or nbsp
self.custom.inpval[fieldname] = inpval or '' self.custom.inpval[fieldname] = inpval if not inpval is None else ''
self.custom.widget[fieldname] = inp self.custom.widget[fieldname] = inp
# if a record is provided and found, as is linkto # if a record is provided and found, as is linkto
@@ -1118,16 +1141,16 @@ class SQLFORM(FORM):
if defaults and len(args) - len(defaults) == 4 or len(args) == 4: if defaults and len(args) - len(defaults) == 4 or len(args) == 4:
table = TABLE() table = TABLE()
for id,a,b,c in xfields: for id,a,b,c in xfields:
raw_b = self.field_parent[id] = b newrows = formstyle(id,a,b,c)
newrows = formstyle(id,a,raw_b,c) self.field_parent[id] = getattr(b,'parent',None)
if type(newrows).__name__ != "tuple": if type(newrows).__name__ != "tuple":
newrows = [newrows] newrows = [newrows]
for newrow in newrows: for newrow in newrows:
table.append(newrow) table.append(newrow)
else: else:
for id,a,b,c in xfields:
self.field_parent[id] = b
table = formstyle(self, xfields) table = formstyle(self, xfields)
for id,a,b,c in xfields:
self.field_parent[id] = getattr(b,'parent',None)
else: else:
raise RuntimeError, 'formstyle not supported' raise RuntimeError, 'formstyle not supported'
return table return table
@@ -1142,6 +1165,7 @@ class SQLFORM(FORM):
dbio=True, dbio=True,
hideerror=False, hideerror=False,
detect_record_change=False, detect_record_change=False,
**kwargs
): ):
""" """
@@ -1166,7 +1190,8 @@ class SQLFORM(FORM):
# implement logic to detect whether record exist but has been modified # implement logic to detect whether record exist but has been modified
# server side # server side
self.record_changed = None self.record_changed = None
if detect_record_change: self.detect_record_change = detect_record_change
if self.detect_record_change:
if self.record: if self.record:
self.record_changed = False self.record_changed = False
serialized = '|'.join(str(self.record[k]) for k in self.table.fields()) serialized = '|'.join(str(self.record[k]) for k in self.table.fields())
@@ -1222,6 +1247,7 @@ class SQLFORM(FORM):
keepvalues, keepvalues,
onvalidation, onvalidation,
hideerror=hideerror, hideerror=hideerror,
**kwargs
) )
self.deleted = \ self.deleted = \
@@ -1231,31 +1257,41 @@ class SQLFORM(FORM):
auch = record_id and self.errors and self.deleted auch = record_id and self.errors and self.deleted
# auch is true when user tries to delete a record if self.record_changed and self.detect_record_change:
# that does not pass validation, yet it should be deleted message_onchange = \
kwargs.setdefault("message_onchange",
if not ret and not auch: current.T("A record change was detected. " +
"Consecutive update self-submissions " +
"are not allowed. Try re-submitting or " +
"refreshing the form page."))
if message_onchange is not None:
current.response.flash = message_onchange
return ret
elif (not ret) and (not auch):
# auch is true when user tries to delete a record
# that does not pass validation, yet it should be deleted
for fieldname in self.fields: for fieldname in self.fields:
field = self.table[fieldname] field = self.table[fieldname]
### this is a workaround! widgets should always have default not None! ### this is a workaround! widgets should always have default not None!
if not field.widget and field.type.startswith('list:') and \ if not field.widget and field.type.startswith('list:') and \
not OptionsWidget.has_options(field): not OptionsWidget.has_options(field):
field.widget = self.widgets.list.widget field.widget = self.widgets.list.widget
if hasattr(field, 'widget') and field.widget and fieldname in request_vars: if field.widget and fieldname in request_vars:
if fieldname in self.vars: if fieldname in self.vars:
value = self.vars[fieldname] value = self.vars[fieldname]
elif self.record: elif self.record:
value = self.record[fieldname] value = self.record[fieldname]
else: else:
value = self.table[fieldname].default value = self.table[fieldname].default
if field.type.startswith('list:') and \ if field.type.startswith('list:') and isinstance(value, str):
isinstance(value, str):
value = [value] value = [value]
row_id = '%s_%s%s' % (self.table, fieldname, SQLFORM.ID_ROW_SUFFIX) row_id = '%s_%s%s' % (self.table, fieldname, SQLFORM.ID_ROW_SUFFIX)
widget = field.widget(field, value) widget = field.widget(field, value)
self.field_parent[row_id].components = [ widget ] parent = self.field_parent[row_id]
self.field_parent[row_id]._traverse(False, hideerror) if parent:
self.custom.widget[ fieldname ] = widget parent.components = [ widget ]
parent._traverse(False, hideerror)
self.custom.widget[fieldname] = widget
self.accepted = ret self.accepted = ret
return ret return ret
@@ -1492,15 +1528,20 @@ class SQLFORM(FORM):
'datetime':['=','!=','<','>','<=','>='], 'datetime':['=','!=','<','>','<=','>='],
'integer':['=','!=','<','>','<=','>='], 'integer':['=','!=','<','>','<=','>='],
'double':['=','!=','<','>','<=','>='], 'double':['=','!=','<','>','<=','>='],
'id':['=','!=','<','>','<=','>='],
'reference':['=','!=','<','>','<=','>='],
'boolean':['=','!=']} 'boolean':['=','!=']}
if fields[0]._db._adapter.dbengine=='google:datastore': if fields[0]._db._adapter.dbengine=='google:datastore':
search_options['string'] = ['=','!=','<','>','<=','>='] search_options['string'] = ['=','!=','<','>','<=','>=']
search_options['text'] = ['=','!=','<','>','<=','>='] search_options['text'] = ['=','!=','<','>','<=','>=']
search_options['list:string'] = ['contains']
search_options['list:integer'] = ['contains']
search_options['list:reference'] = ['contains']
criteria = [] criteria = []
selectfields = [] selectfields = []
for field in fields: for field in fields:
name = str(field).replace('.','-') name = str(field).replace('.','-')
options = search_options.get(field.type,None) options = search_options.get(field.type.split(' ')[0],None)
if options: if options:
label = isinstance(field.label,str) and T(field.label) or field.label label = isinstance(field.label,str) and T(field.label) or field.label
selectfields.append(OPTION(label, _value=str(field))) selectfields.append(OPTION(label, _value=str(field)))
@@ -1789,16 +1830,19 @@ class SQLFORM(FORM):
table = db[request.args[-2]] table = db[request.args[-2]]
record = table(request.args[-1]) or redirect(URL('error')) record = table(request.args[-1]) or redirect(URL('error'))
sqlformargs.update(editargs) sqlformargs.update(editargs)
update_form = SQLFORM(table, record, upload=upload, ignore_rw=ignore_rw, update_form = SQLFORM(
formstyle=formstyle, deletable=deletable, table,
_class='web2py_form', record, upload=upload, ignore_rw=ignore_rw,
submit_button=T('Submit'), formstyle=formstyle, deletable=deletable,
delete_label=T('Check to delete'), _class='web2py_form',
**sqlformargs) submit_button=T('Submit'),
update_form.process(formname=formname, delete_label=T('Check to delete'),
onvalidation=onvalidation, **sqlformargs)
onsuccess=onupdate, update_form.process(
next=referrer) formname=formname,
onvalidation=onvalidation,
onsuccess=onupdate,
next=referrer)
res = DIV(buttons(view=details, record=record), res = DIV(buttons(view=details, record=record),
update_form, formfooter, _class=_class) update_form, formfooter, _class=_class)
res.create_form = create_form res.create_form = create_form
@@ -1852,7 +1896,7 @@ class SQLFORM(FORM):
rows = dbset.select(cacheable=True) rows = dbset.select(cacheable=True)
else: else:
rows = dbset.select(left=left,orderby=orderby, rows = dbset.select(left=left,orderby=orderby,
cacheable=True*columns) cacheable=True,*columns)
if export_type in exportManager: if export_type in exportManager:
value = exportManager[export_type] value = exportManager[export_type]
@@ -1890,7 +1934,7 @@ class SQLFORM(FORM):
search_widget = search_widget[tablename] search_widget = search_widget[tablename]
if search_widget=='default': if search_widget=='default':
search_menu = SQLFORM.search_menu(sfields) search_menu = SQLFORM.search_menu(sfields)
search_widget = lambda sfield, url: CAT(add,FORM( search_widget = lambda sfield, url: CAT(FORM(
INPUT(_name='keywords',_value=request.vars.keywords, INPUT(_name='keywords',_value=request.vars.keywords,
_id='web2py_keywords',_onfocus="jQuery('#w2p_query_fields').change();jQuery('#w2p_query_panel').slideDown();"), _id='web2py_keywords',_onfocus="jQuery('#w2p_query_fields').change();jQuery('#w2p_query_panel').slideDown();"),
INPUT(_type='submit',_value=T('Search'),_class="btn"), INPUT(_type='submit',_value=T('Search'),_class="btn"),
@@ -1898,6 +1942,7 @@ class SQLFORM(FORM):
_onclick="jQuery('#web2py_keywords').val('');"), _onclick="jQuery('#web2py_keywords').val('');"),
_method="GET",_action=url),search_menu) _method="GET",_action=url),search_menu)
form = search_widget and search_widget(sfields,url()) or '' form = search_widget and search_widget(sfields,url()) or ''
console.append(add)
console.append(form) console.append(form)
keywords = request.vars.get('keywords','') keywords = request.vars.get('keywords','')
try: try:
@@ -1942,8 +1987,7 @@ class SQLFORM(FORM):
if columns and not str(field) in columns: continue if columns and not str(field) in columns: continue
if not field.readable: continue if not field.readable: continue
key = str(field) key = str(field)
header = headers.get(str(field), header = headers.get(str(field), field.label or key)
hasattr(field,'label') and field.label or key)
if sortable: if sortable:
if key == order: if key == order:
key, marker = '~'+order, sorter_icons[0] key, marker = '~'+order, sorter_icons[0]
@@ -2003,7 +2047,7 @@ class SQLFORM(FORM):
table_fields = [f for f in fields if f._tablename in tablenames] table_fields = [f for f in fields if f._tablename in tablenames]
rows = dbset.select(left=left,orderby=orderby, rows = dbset.select(left=left,orderby=orderby,
groupby=groupby,limitby=limitby, groupby=groupby,limitby=limitby,
cacheable=True,*table_fields) *table_fields)
except SyntaxError: except SyntaxError:
rows = None rows = None
error = T("Query Not Supported") error = T("Query Not Supported")
@@ -2226,7 +2270,9 @@ class SQLFORM(FORM):
else: else:
break break
if nargs>len(args)+1: if nargs>len(args)+1:
query = (field == id) query = (field == id)
if isinstance(linked_tables,dict):
linked_tables = linked_tables.get(table._tablename,[])
if linked_tables is None or referee in linked_tables: if linked_tables is None or referee in linked_tables:
field.represent = lambda id,r=None,referee=referee,rep=field.represent: A(callable(rep) and rep(id) or id,_class=trap_class(),_href=url(args=['view',referee,id])) field.represent = lambda id,r=None,referee=referee,rep=field.represent: A(callable(rep) and rep(id) or id,_class=trap_class(),_href=url(args=['view',referee,id]))
except (KeyError,ValueError,TypeError): except (KeyError,ValueError,TypeError):
@@ -2251,6 +2297,8 @@ class SQLFORM(FORM):
if rfield.readable: if rfield.readable:
check[rfield.tablename] = \ check[rfield.tablename] = \
check.get(rfield.tablename,[])+[rfield.name] check.get(rfield.tablename,[])+[rfield.name]
if isinstance(linked_tables,dict):
linked_tables = linked_tables.get(table._tablename,[])
for tablename in sorted(check): for tablename in sorted(check):
linked_fieldnames = check[tablename] linked_fieldnames = check[tablename]
tb = db[tablename] tb = db[tablename]
@@ -2366,6 +2414,7 @@ class SQLTABLE(TABLE):
extracolumns=None, extracolumns=None,
selectid=None, selectid=None,
renderstyle=False, renderstyle=False,
cid=None,
**attributes **attributes
): ):
@@ -2403,7 +2452,7 @@ class SQLTABLE(TABLE):
row.append(TH(coldict['label'],**attrcol)) row.append(TH(coldict['label'],**attrcol))
elif orderby: elif orderby:
row.append(TH(A(headers.get(c, c), row.append(TH(A(headers.get(c, c),
_href=th_link+'?orderby=' + c))) _href=th_link+'?orderby=' + c, cid=cid)))
else: else:
row.append(TH(headers.get(c, c))) row.append(TH(headers.get(c, c)))
@@ -2698,8 +2747,3 @@ class ExporterXML(ExportClass):
out.write('</row>\n') out.write('</row>\n')
out.write('</rows>') out.write('</rows>')
return str(out.getvalue()) return str(out.getvalue())
+6 -19
View File
@@ -38,30 +38,17 @@ class Storage(dict):
>>> print o.a >>> print o.a
None None
""" """
__slots__=() __slots__=()
__setattr__ = dict.__setitem__ __setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__ __delattr__ = dict.__delitem__
__getitem__ = dict.get __getitem__ = dict.get
__getattr__ = dict.get __getattr__ = dict.get
__repr__ = lambda self: '<Storage %s>' % dict.__repr__(self) __repr__ = lambda self: '<Storage %s>' % dict.__repr__(self)
__getstate__ = dict # http://stackoverflow.com/questions/5247250/why-does-pickle-getstate-accept-as-a-return-value-the-very-instance-it-requi
__setstate__ = dict.update __getstate__ = lambda self: None
# def __getattr__(self, key): __copy__ = lambda self: Storage(self)
# return dict.get(self, key, None)
# def __setattr__(self, key, value):
# self[key] = value
# def __getitem__(self, key):
# return dict.get(self, key, None)
# def __delattr__(self, key):
# del self[key]
# def __repr__(self):
# return '<Storage %s>' % dict.__repr__(self)
# def __getstate__(self):
# return dict(self)
# def __setstate__(self,values):
# self.update(values)
def getlist(self,key): def getlist(self,key):
""" """
Return a Storage value as a list. Return a Storage value as a list.
+39 -34
View File
@@ -43,10 +43,11 @@ def stream_file_or_304_or_206(
chunk_size = DEFAULT_CHUNK_SIZE, chunk_size = DEFAULT_CHUNK_SIZE,
request = None, request = None,
headers = {}, headers = {},
status = 200,
error_message = None, error_message = None,
): ):
if error_message is None: if error_message is None:
error_message = rewrite.thread.routes.error_message % 'invalid request' error_message = rewrite.THREAD_LOCAL.routes.error_message % 'invalid request'
try: try:
fp = open(static_file) fp = open(static_file)
except IOError, e: except IOError, e:
@@ -60,56 +61,60 @@ def stream_file_or_304_or_206(
fp.close() fp.close()
stat_file = os.stat(static_file) stat_file = os.stat(static_file)
fsize = stat_file[stat.ST_SIZE] fsize = stat_file[stat.ST_SIZE]
mtime = time.strftime('%a, %d %b %Y %H:%M:%S GMT', modified = stat_file[stat.ST_MTIME]
time.gmtime(stat_file[stat.ST_MTIME])) mtime = time.strftime('%a, %d %b %Y %H:%M:%S GMT',time.gmtime(modified))
headers.setdefault('Content-Type', contenttype(static_file)) headers.setdefault('Content-Type', contenttype(static_file))
headers.setdefault('Last-Modified', mtime) headers.setdefault('Last-Modified', mtime)
headers.setdefault('Pragma', 'cache') headers.setdefault('Pragma', 'cache')
headers.setdefault('Cache-Control', 'private') headers.setdefault('Cache-Control', 'private')
if request and request.env.http_if_modified_since == mtime: # if this is a normal response and not a respnse to an error page
raise HTTP(304, **{'Content-Type': headers['Content-Type']}) if status == 200:
if request and request.env.http_if_modified_since == mtime:
raise HTTP(304, **{'Content-Type': headers['Content-Type']})
elif request and request.env.http_range: elif request and request.env.http_range:
start_items = regex_start_range.findall(request.env.http_range) start_items = regex_start_range.findall(request.env.http_range)
if not start_items: if not start_items:
start_items = [0] start_items = [0]
stop_items = regex_stop_range.findall(request.env.http_range) stop_items = regex_stop_range.findall(request.env.http_range)
if not stop_items or int(stop_items[0]) > fsize - 1: if not stop_items or int(stop_items[0]) > fsize - 1:
stop_items = [fsize - 1] stop_items = [fsize - 1]
part = (int(start_items[0]), int(stop_items[0]), fsize) part = (int(start_items[0]), int(stop_items[0]), fsize)
bytes = part[1] - part[0] + 1 bytes = part[1] - part[0] + 1
try: try:
stream = open(static_file, 'rb') stream = open(static_file, 'rb')
except IOError, e: except IOError, e:
if e[0] in (errno.EISDIR, errno.EACCES): if e[0] in (errno.EISDIR, errno.EACCES):
raise HTTP(403) raise HTTP(403)
else: else:
raise HTTP(404) raise HTTP(404)
stream.seek(part[0]) stream.seek(part[0])
headers['Content-Range'] = 'bytes %i-%i/%i' % part headers['Content-Range'] = 'bytes %i-%i/%i' % part
headers['Content-Length'] = '%i' % bytes headers['Content-Length'] = '%i' % bytes
status = 206 status = 206
else: # in all the other cases (not 304, not 206, but 200 or error page)
if status != 206:
enc = request.env.http_accept_encoding
if enc and 'gzip' in enc and not 'Content-Encoding' in headers:
gzipped = static_file + '.gz'
if os.path.isfile(gzipped) and os.path.getmtime(gzipped)>modified:
static_file = gzipped
fsize = os.path.getsize(gzipped)
headers['Content-Encoding'] = 'gzip'
headers['Vary'] = 'Accept-Encoding'
try: try:
stream = open(static_file, 'rb') stream = open(static_file, 'rb')
except IOError, e: except IOError, e:
# this better does not happer when returning an error page ;-)
if e[0] in (errno.EISDIR, errno.EACCES): if e[0] in (errno.EISDIR, errno.EACCES):
raise HTTP(403) raise HTTP(403)
else: else:
raise HTTP(404) raise HTTP(404)
headers['Content-Length'] = fsize headers['Content-Length'] = fsize
bytes = None bytes = None
status = 200
if request and request.env.web2py_use_wsgi_file_wrapper: if request and request.env.web2py_use_wsgi_file_wrapper:
wrapped = request.env.wsgi_file_wrapper(stream, chunk_size) wrapped = request.env.wsgi_file_wrapper(stream, chunk_size)
else: else:
wrapped = streamer(stream, chunk_size=chunk_size, bytes=bytes) wrapped = streamer(stream, chunk_size=chunk_size, bytes=bytes)
raise HTTP(status, wrapped, **headers) raise HTTP(status, wrapped, **headers)
+1 -1
View File
@@ -416,7 +416,7 @@ class TemplateParser(object):
# Allow Views to include other views dynamically # Allow Views to include other views dynamically
context = self.context context = self.context
if current and not "response" in context: if current and not "response" in context:
context["response"] = current.response context["response"] = getattr(current,'response',None)
# Get the filename; filename looks like ``"template.html"``. # Get the filename; filename looks like ``"template.html"``.
# We need to eval to remove the quotes and get the string type. # We need to eval to remove the quotes and get the string type.
+12 -7
View File
@@ -311,12 +311,13 @@ class TestDatetime(unittest.TestCase):
9, 30)), 3) 9, 30)), 3)
self.assertEqual(len(db(db.t.a == datetime.datetime(1971, 12, self.assertEqual(len(db(db.t.a == datetime.datetime(1971, 12,
21, 11, 30)).select()), 1) 21, 11, 30)).select()), 1)
self.assertEqual(len(db(db.t.a.year() == 1971).select()), 2) self.assertEqual(db(db.t.a.year() == 1971).count(), 2)
self.assertEqual(len(db(db.t.a.month() == 12).select()), 2) self.assertEqual(db(db.t.a.month() == 12).count(), 2)
self.assertEqual(len(db(db.t.a.day() == 21).select()), 3) self.assertEqual(db(db.t.a.day() == 21).count(), 3)
self.assertEqual(len(db(db.t.a.hour() == 11).select()), 1) self.assertEqual(db(db.t.a.hour() == 11).count(), 1)
self.assertEqual(len(db(db.t.a.minutes() == 30).select()), 3) self.assertEqual(db(db.t.a.minutes() == 30).count(), 3)
self.assertEqual(len(db(db.t.a.seconds() == 0).select()), 3) self.assertEqual(db(db.t.a.seconds() == 0).count(), 3)
self.assertEqual(db(db.t.a.epoch()<365*24*3600).count(),1)
db.t.drop() db.t.drop()
@@ -329,7 +330,7 @@ class TestExpressions(unittest.TestCase):
self.assertEqual(db.t.insert(a=2), 2) self.assertEqual(db.t.insert(a=2), 2)
self.assertEqual(db.t.insert(a=3), 3) self.assertEqual(db.t.insert(a=3), 3)
self.assertEqual(db(db.t.a == 3).update(a=db.t.a + 1), 1) self.assertEqual(db(db.t.a == 3).update(a=db.t.a + 1), 1)
self.assertEqual(len(db(db.t.a == 4).select()), 1) self.assertEqual(db(db.t.a == 4).count(), 1)
db.t.drop() db.t.drop()
@@ -447,18 +448,22 @@ class TestMigrations(unittest.TestCase):
db = DAL('sqlite://.storage.db') db = DAL('sqlite://.storage.db')
db.define_table('t', Field('a'), migrate='.storage.table') db.define_table('t', Field('a'), migrate='.storage.table')
db.commit() db.commit()
db.close()
db = DAL('sqlite://.storage.db') db = DAL('sqlite://.storage.db')
db.define_table('t', Field('a'), Field('b'), db.define_table('t', Field('a'), Field('b'),
migrate='.storage.table') migrate='.storage.table')
db.commit() db.commit()
db.close()
db = DAL('sqlite://.storage.db') db = DAL('sqlite://.storage.db')
db.define_table('t', Field('a'), Field('b', 'text'), db.define_table('t', Field('a'), Field('b', 'text'),
migrate='.storage.table') migrate='.storage.table')
db.commit() db.commit()
db.close()
db = DAL('sqlite://.storage.db') db = DAL('sqlite://.storage.db')
db.define_table('t', Field('a'), migrate='.storage.table') db.define_table('t', Field('a'), migrate='.storage.table')
db.t.drop() db.t.drop()
db.commit() db.commit()
db.close()
def tearDown(self): def tearDown(self):
if os.path.exists('.storage.db'): if os.path.exists('.storage.db'):
+7 -7
View File
@@ -163,17 +163,17 @@ default_application = 'defapp'
self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path\]', filter_url, 'http://domain.com/init/bad!ctl') self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path\]', filter_url, 'http://domain.com/init/bad!ctl')
self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path\]', filter_url, 'http://domain.com/init/ctlr/bad!fcn') self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path\]', filter_url, 'http://domain.com/init/ctlr/bad!fcn')
self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path\]', filter_url, 'http://domain.com/init/ctlr/fcn.bad!ext') self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path\]', filter_url, 'http://domain.com/init/ctlr/fcn.bad!ext')
self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path \(args\)\]', filter_url, 'http://domain.com/appc/init/fcn/bad!arg') self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path\]', filter_url, 'http://domain.com/appc/init/fcn/bad!arg')
except AttributeError: except AttributeError:
pass pass
self.assertEqual(filter_url('http://domain.com/welcome/default/fcn_1'), "/welcome/default/fcn_1") self.assertEqual(filter_url('http://domain.com/welcome/default/fcn_1'), "/welcome/default/fcn_1")
self.assertRaises(HTTP, filter_url, 'http://domain.com/welcome/default/fcn-1') #self.assertRaises(HTTP, filter_url, 'http://domain.com/welcome/default/fcn-1')
try: #try:
# 2.7+ only # # 2.7+ only
self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path\]', filter_url, 'http://domain.com/welcome/default/fcn-1') # self.assertRaisesRegexp(HTTP, '400 BAD REQUEST \[invalid path\]', filter_url, 'http://domain.com/welcome/default/fcn-1')
except AttributeError: #except AttributeError:
pass # pass
def test_routes_error(self): def test_routes_error(self):
''' '''
+12
View File
@@ -46,6 +46,18 @@ class TestWeb(unittest.TestCase):
client.get('site') client.get('site')
client.get('design/welcome') client.get('design/welcome')
class TestStaticCacheControl(unittest.TestCase):
def testWebClient(self):
s=WebClient('http://127.0.0.1:8000/welcome/')
s.get('static/js/web2py.js')
assert('expires' not in s.headers)
assert(not s.headers['cache-control'].startswith('max-age'))
text = s.text
s.get('static/_1.2.3/js/web2py.js')
assert(text == s.text)
assert('expires' in s.headers)
assert(s.headers['cache-control'].startswith('max-age'))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+185 -101
View File
@@ -264,6 +264,7 @@ class Mail(object):
cc=None, cc=None,
bcc=None, bcc=None,
reply_to=None, reply_to=None,
sender='%(sender)s',
encoding='utf-8', encoding='utf-8',
raw=False, raw=False,
headers={} headers={}
@@ -606,7 +607,8 @@ class Mail(object):
# no cryptography process as usual # no cryptography process as usual
payload=payload_in payload=payload_in
payload['From'] = encoded_or_raw(self.settings.sender.decode(encoding)) sender = sender % dict(sender=self.settings.sender)
payload['From'] = encoded_or_raw(sender.decode(encoding))
origTo = to[:] origTo = to[:]
if to: if to:
payload['To'] = encoded_or_raw(', '.join(to).decode(encoding)) payload['To'] = encoded_or_raw(', '.join(to).decode(encoding))
@@ -623,10 +625,10 @@ class Mail(object):
for k,v in headers.iteritems(): for k,v in headers.iteritems():
payload[k] = encoded_or_raw(v.decode(encoding)) payload[k] = encoded_or_raw(v.decode(encoding))
result = {} result = {}
try: try:
if self.settings.server == 'logging': if self.settings.server == 'logging':
logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \ logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \
('-'*40,self.settings.sender, ('-'*40,sender,
', '.join(to),subject, ', '.join(to),subject,
text or html,'-'*40)) text or html,'-'*40))
elif self.settings.server == 'gae': elif self.settings.server == 'gae':
@@ -794,6 +796,10 @@ def addrow(form, a, b, c, style, _id, position=-1):
DIV(b, _class='w2p_fw'), DIV(b, _class='w2p_fw'),
DIV(c, _class='w2p_fc'), DIV(c, _class='w2p_fc'),
_id = _id)) _id = _id))
elif style == "bootstrap":
form[0].insert(position, DIV(LABEL(a,_class='control-label'),
DIV(b,SPAN(c, _class='inline-help'),_class='controls'),
_class='control-group',_id = _id))
else: else:
form[0].insert(position, TR(TD(LABEL(a),_class='w2p_fl'), form[0].insert(position, TR(TD(LABEL(a),_class='w2p_fl'),
TD(b,_class='w2p_fw'), TD(b,_class='w2p_fw'),
@@ -1044,7 +1050,7 @@ class Auth(object):
def __init__(self, environment=None, db=None, mailer=True, def __init__(self, environment=None, db=None, mailer=True,
hmac_key=None, controller='default', function='user', hmac_key=None, controller='default', function='user',
cas_provider=None, signature=True): cas_provider=None, signature=True, secure=False):
""" """
auth=Auth(db) auth=Auth(db)
@@ -1064,6 +1070,8 @@ class Auth(object):
session = current.session session = current.session
auth = session.auth auth = session.auth
self.user_groups = auth and auth.user_groups or {} self.user_groups = auth and auth.user_groups or {}
if secure:
request.requires_https()
if auth and auth.last_visit and auth.last_visit + \ if auth and auth.last_visit and auth.last_visit + \
datetime.timedelta(days=0, seconds=auth.expiration) > request.now: datetime.timedelta(days=0, seconds=auth.expiration) > request.now:
self.user = auth.user self.user = auth.user
@@ -1191,7 +1199,10 @@ class Auth(object):
'reset_password','request_reset_password', 'reset_password','request_reset_password',
'change_password','profile','groups', 'change_password','profile','groups',
'impersonate','not_authorized'): 'impersonate','not_authorized'):
return getattr(self,args[0])() if len(request.args) >= 2 and args[0]=='impersonate':
return getattr(self,args[0])(request.args[1])
else:
return getattr(self,args[0])()
elif args[0]=='cas' and not self.settings.cas_provider: elif args[0]=='cas' and not self.settings.cas_provider:
if args(1) == self.settings.cas_actions['login']: if args(1) == self.settings.cas_actions['login']:
return self.cas_login(version=2) return self.cas_login(version=2)
@@ -1598,7 +1609,7 @@ class Auth(object):
if settings.cas_provider: ### THIS IS NOT LAZY if settings.cas_provider: ### THIS IS NOT LAZY
settings.actions_disabled = \ settings.actions_disabled = \
['profile','register','change_password', ['profile','register','change_password',
'request_reset_password'] 'request_reset_password','retrieve_username']
from gluon.contrib.login_methods.cas_auth import CasAuth from gluon.contrib.login_methods.cas_auth import CasAuth
maps = settings.cas_maps maps = settings.cas_maps
if not maps: if not maps:
@@ -1616,6 +1627,7 @@ class Auth(object):
urlbase = settings.cas_provider, urlbase = settings.cas_provider,
actions=actions, actions=actions,
maps=maps) maps=maps)
return self
def log_event(self, description, vars=None, origin='auth'): def log_event(self, description, vars=None, origin='auth'):
""" """
@@ -1700,7 +1712,8 @@ class Auth(object):
""" """
login the user = db.auth_user(id) login the user = db.auth_user(id)
""" """
# user=Storage(self.table_user()._filter_fields(user,id=True)) user = Storage(self.table_user()._filter_fields(user,id=True))
if 'password' in user: del user.password
current.session.auth = Storage( current.session.auth = Storage(
user = user, user = user,
last_visit = current.request.now, last_visit = current.request.now,
@@ -1901,20 +1914,33 @@ class Auth(object):
if self.settings.remember_me_form: if self.settings.remember_me_form:
## adds a new input checkbox "remember me for longer" ## adds a new input checkbox "remember me for longer"
addrow(form,XML("&nbsp;"), if self.settings.formstyle != 'bootstrap':
DIV(XML("&nbsp;"), addrow(form,XML("&nbsp;"),
INPUT(_type='checkbox', DIV(XML("&nbsp;"),
_class='checkbox', INPUT(_type='checkbox',
_id="auth_user_remember", _class='checkbox',
_name="remember", _id="auth_user_remember",
), _name="remember",
XML("&nbsp;&nbsp;"), ),
XML("&nbsp;&nbsp;"),
LABEL(
self.messages.label_remember_me,
_for="auth_user_remember",
)),"",
self.settings.formstyle,
'auth_user_remember__row')
elif self.settings.formstyle == 'bootstrap':
addrow(form,
"",
LABEL( LABEL(
self.messages.label_remember_me, INPUT(_type='checkbox',
_for="auth_user_remember", _id="auth_user_remember",
)),"", _name="remember"),
self.settings.formstyle, self.messages.label_remember_me,
'auth_user_remember__row') _class="checkbox"),
"",
self.settings.formstyle,
'auth_user_remember__row')
captcha = self.settings.login_captcha or \ captcha = self.settings.login_captcha or \
(self.settings.login_captcha!=False and self.settings.captcha) (self.settings.login_captcha!=False and self.settings.captcha)
@@ -2131,6 +2157,9 @@ class Auth(object):
repr(request.vars.get(passfield, None)), repr(request.vars.get(passfield, None)),
error_message=self.messages.mismatched_password)) error_message=self.messages.mismatched_password))
if formstyle == 'bootstrap' :
form.custom.widget.password_two['_class'] = 'input-xlarge'
addrow(form, self.messages.verify_password + self.settings.label_separator, addrow(form, self.messages.verify_password + self.settings.label_separator,
form.custom.widget.password_two, form.custom.widget.password_two,
self.messages.verify_password_comment, self.messages.verify_password_comment,
@@ -2420,7 +2449,7 @@ class Auth(object):
session = current.session session = current.session
if next is DEFAULT: if next is DEFAULT:
next = self.settings.reset_password_next next = self.next or self.settings.reset_password_next
try: try:
key = request.vars.key or getarg(-1) key = request.vars.key or getarg(-1)
t0 = int(key.split('-')[0]) t0 = int(key.split('-')[0])
@@ -2723,7 +2752,7 @@ class Auth(object):
self.user = session.auth.user self.user = session.auth.user
if requested_id is DEFAULT and not request.post_vars: if requested_id is DEFAULT and not request.post_vars:
return SQLFORM.factory(Field('user_id', 'integer')) return SQLFORM.factory(Field('user_id', 'integer'))
return self.user return SQLFORM(table_user, user.id, readonly=True)
def update_groups(self): def update_groups(self):
if not self.user: if not self.user:
@@ -3179,7 +3208,7 @@ class Auth(object):
def wiki(self, def wiki(self,
slug=None, slug=None,
env=None, env=None,
render=None, render='markmin',
manage_permissions=False, manage_permissions=False,
force_prefix='', force_prefix='',
restrict_search=False, restrict_search=False,
@@ -3318,6 +3347,7 @@ class Crud(object):
message=DEFAULT, message=DEFAULT,
deletable=DEFAULT, deletable=DEFAULT,
formname=DEFAULT, formname=DEFAULT,
**attributes
): ):
""" """
method: Crud.update(table, record, [next=DEFAULT method: Crud.update(table, record, [next=DEFAULT
@@ -3370,7 +3400,8 @@ class Crud(object):
deletable=deletable, deletable=deletable,
upload=self.settings.download_url, upload=self.settings.download_url,
formstyle=self.settings.formstyle, formstyle=self.settings.formstyle,
separator=self.settings.label_separator separator=self.settings.label_separator,
**attributes
) )
self.accepted = False self.accepted = False
self.deleted = False self.deleted = False
@@ -3428,6 +3459,7 @@ class Crud(object):
log=DEFAULT, log=DEFAULT,
message=DEFAULT, message=DEFAULT,
formname=DEFAULT, formname=DEFAULT,
**attributes
): ):
""" """
method: Crud.create(table, [next=DEFAULT [, onvalidation=DEFAULT method: Crud.create(table, [next=DEFAULT [, onvalidation=DEFAULT
@@ -3454,6 +3486,7 @@ class Crud(object):
message=message, message=message,
deletable=False, deletable=False,
formname=formname, formname=formname,
**attributes
) )
def read(self, table, record): def read(self, table, record):
@@ -4313,34 +4346,39 @@ def prettydate(d,T=lambda x:x):
return '' return ''
else: else:
return '[invalid date]' return '[invalid date]'
if dt.days < 0:
suffix = ' from now'
dt = -dt
else:
suffix = ' ago'
if dt.days >= 2*365: if dt.days >= 2*365:
return T('%d years ago') % int(dt.days / 365) return T('%d years'+suffix) % int(dt.days / 365)
elif dt.days >= 365: elif dt.days >= 365:
return T('1 year ago') return T('1 year'+suffix)
elif dt.days >= 60: elif dt.days >= 60:
return T('%d months ago') % int(dt.days / 30) return T('%d months'+suffix) % int(dt.days / 30)
elif dt.days > 21: elif dt.days > 21:
return T('1 month ago') return T('1 month'+suffix)
elif dt.days >= 14: elif dt.days >= 14:
return T('%d weeks ago') % int(dt.days / 7) return T('%d weeks'+suffix) % int(dt.days / 7)
elif dt.days >= 7: elif dt.days >= 7:
return T('1 week ago') return T('1 week'+suffix)
elif dt.days > 1: elif dt.days > 1:
return T('%d days ago') % dt.days return T('%d days'+suffix) % dt.days
elif dt.days == 1: elif dt.days == 1:
return T('1 day ago') return T('1 day'+suffix)
elif dt.seconds >= 2*60*60: elif dt.seconds >= 2*60*60:
return T('%d hours ago') % int(dt.seconds / 3600) return T('%d hours'+suffix) % int(dt.seconds / 3600)
elif dt.seconds >= 60*60: elif dt.seconds >= 60*60:
return T('1 hour ago') return T('1 hour'+suffix)
elif dt.seconds >= 2*60: elif dt.seconds >= 2*60:
return T('%d minutes ago') % int(dt.seconds / 60) return T('%d minutes'+suffix) % int(dt.seconds / 60)
elif dt.seconds >= 60: elif dt.seconds >= 60:
return T('1 minute ago') return T('1 minute'+suffix)
elif dt.seconds > 1: elif dt.seconds > 1:
return T('%d seconds ago') % dt.seconds return T('%d seconds'+suffix) % dt.seconds
elif dt.seconds == 1: elif dt.seconds == 1:
return T('1 second ago') return T('1 second'+suffix)
else: else:
return T('now') return T('now')
@@ -4539,6 +4577,7 @@ class Wiki(object):
self.env['component'] = Wiki.component self.env['component'] = Wiki.component
if render == 'markmin': render=self.markmin_render if render == 'markmin': render=self.markmin_render
elif render == 'html': render=self.html_render elif render == 'html': render=self.html_render
self.render = render
self.auth = auth self.auth = auth
if self.auth.user: if self.auth.user:
self.force_prefix = force_prefix % self.auth.user self.force_prefix = force_prefix % self.auth.user
@@ -4548,47 +4587,56 @@ class Wiki(object):
perms = self.manage_permissions = manage_permissions perms = self.manage_permissions = manage_permissions
self.restrict_search = restrict_search self.restrict_search = restrict_search
db = auth.db db = auth.db
table_definitions = { table_definitions = [
'wiki_page':{ ('wiki_page',{
'args':[ 'args':[
Field('slug', Field('slug',
requires=[IS_SLUG(), requires=[IS_SLUG(),
IS_NOT_IN_DB(db,'wiki_page.slug')], IS_NOT_IN_DB(db,'wiki_page.slug')],
readable=False,writable=False), readable=False,writable=False),
Field('title',unique=True), Field('title',unique=True),
Field('body','text',notnull=True), Field('body','text',notnull=True),
Field('tags','list:string'), Field('tags','list:string'),
Field('can_read','list:string', Field('can_read','list:string',
writable=perms, writable=perms,
readable=perms, readable=perms,
default=[Wiki.everybody]), default=[Wiki.everybody]),
Field('can_edit', 'list:string', Field('can_edit', 'list:string',
writable=perms,readable=perms, writable=perms,readable=perms,
default=[Wiki.everybody]), default=[Wiki.everybody]),
Field('changelog'), Field('changelog'),
Field('html','text',compute=render, Field('html','text',compute=render,
readable=False, writable=False), readable=False, writable=False),
auth.signature], auth.signature],
'vars':{'format':'%(title)s'}}, 'vars':{'format':'%(title)s'}}),
'wiki_tag':{ ('wiki_tag',{
'args':[ 'args':[
Field('name'), Field('name'),
Field('wiki_page','reference wiki_page'), Field('wiki_page','reference wiki_page'),
auth.signature], auth.signature],
'vars':{'format':'%(name)s'}}, 'vars':{'format':'%(name)s'}}),
'wiki_media':{ ('wiki_media',{
'args':[ 'args':[
Field('wiki_page','reference wiki_page'), Field('wiki_page','reference wiki_page'),
Field('title',required=True), Field('title',required=True),
Field('file','upload',required=True), Field('filename','upload',required=True),
auth.signature], auth.signature],
'vars':{'format':'%(title)s'}} 'vars':{'format':'%(title)s'}})
} ]
# define only non-existent tables # define only non-existent tables
for key, value in table_definitions.iteritems(): for key, value in table_definitions:
args = []
if not key in db.tables(): if not key in db.tables():
db.define_table(key, *value['args'], **value['vars']) # look for wiki_ extra fields in auth.settings
extra_fields = auth.settings.extra_fields
if extra_fields:
if key in extra_fields:
if extra_fields[key]:
for field in extra_fields[key]:
args.append(field)
args += value['args']
db.define_table(key, *args, **value['vars'])
def update_tags_insert(page,id,db=db): def update_tags_insert(page,id,db=db):
for tag in page.tags or []: for tag in page.tags or []:
@@ -4665,17 +4713,19 @@ class Wiki(object):
) )
elif zero=='_cloud': elif zero=='_cloud':
return self.cloud() return self.cloud()
elif zero == '_preview':
return self.preview(self.render)
def first_paragraph(self,page): def first_paragraph(self,page):
if not self.can_read(page): if not self.can_read(page):
mm = page.body.replace('\r','') mm = (page.body or '').replace('\r','')
ps = [p for p in mm.split('\n\n') \ ps = [p for p in mm.split('\n\n') \
if not p.startswith('#') and p.strip()] if not p.startswith('#') and p.strip()]
if ps: return ps[0] if ps: return ps[0]
return '' return ''
def fix_hostname(self,body): def fix_hostname(self,body):
return body.replace('://HOSTNAME','://%s' % self.host) return (body or '').replace('://HOSTNAME','://%s' % self.host)
def read(self,slug): def read(self,slug):
if slug in '_cloud': if slug in '_cloud':
@@ -4730,7 +4780,8 @@ class Wiki(object):
db.wiki_page.title.default = title_guess db.wiki_page.title.default = title_guess
db.wiki_page.slug.default = slug db.wiki_page.slug.default = slug
if slug == 'wiki-menu': if slug == 'wiki-menu':
db.wiki_page.body.default = '- Menu Item > @////index\n- - Submenu > http://web2py.com' db.wiki_page.body.default = \
'- Menu Item > @////index\n- - Submenu > http://web2py.com'
else: else:
db.wiki_page.body.default = '## %s\n\npage content' % title_guess db.wiki_page.body.default = '## %s\n\npage content' % title_guess
vars = current.request.post_vars vars = current.request.post_vars
@@ -4744,17 +4795,41 @@ class Wiki(object):
elif form.accepted: elif form.accepted:
current.session.flash = 'page created' current.session.flash = 'page created'
redirect(URL(args=slug)) redirect(URL(args=slug))
return dict(content=form) script = """
$(function() {
if (!$('#wiki_page_body').length) return;
var pagecontent = $('#wiki_page_body');
pagecontent.css('font-family', 'Monaco,Menlo,Consolas,"Courier New",monospace');
var prevbutton = $('<button class="btn nopreview">Preview</button>');
var preview = $('<div id="preview"></div>').hide();
var table = $('form');
prevbutton.insertBefore(table);
preview.insertBefore(table);
prevbutton.on('click', function(e) {
e.preventDefault();
if (prevbutton.hasClass('nopreview')) {
prevbutton.addClass('preview').removeClass('nopreview').html('Edit Source');
web2py_ajax_page('post', '%(url)s', {body : $('#wiki_page_body').val()}, 'preview');
table.fadeOut('medium', function() {preview.fadeIn()});
} else {
prevbutton.addClass('nopreview').removeClass('preview').html('Preview');
preview.fadeOut('medium', function() {table.fadeIn()});
}
})
})
""" % dict(url=URL(args=('_preview')))
return dict(content=TAG[''](form, SCRIPT(script)))
def editmedia(self,slug): def editmedia(self,slug):
auth = self.auth auth = self.auth
db = auth.db db = auth.db
page = db.wiki_page(slug=slug) page = db.wiki_page(slug=slug)
if not (page and self.can_edit(page)): return self.not_authorized(page) if not (page and self.can_edit(page)): return self.not_authorized(page)
self.auth.db.wiki_media.id.represent = lambda id,row:\ self.auth.db.wiki_media.id.represent = lambda id,row: \
id if not row.filename else \
SPAN('@////%i/%s.%s' % \ SPAN('@////%i/%s.%s' % \
(id,IS_SLUG.urlify(row.title.split('.')[0]), (id,IS_SLUG.urlify(row.title.split('.')[0]),
row.file.split('.')[-1])) row.filename.split('.')[-1]))
self.auth.db.wiki_media.wiki_page.default = page.id self.auth.db.wiki_media.wiki_page.default = page.id
self.auth.db.wiki_media.wiki_page.writable = False self.auth.db.wiki_media.wiki_page.writable = False
content = SQLFORM.grid( content = SQLFORM.grid(
@@ -4799,7 +4874,7 @@ class Wiki(object):
if self.manage_permissions: if self.manage_permissions:
page = db.wiki_page(media.wiki_page) page = db.wiki_page(media.wiki_page)
if not self.can_read(page): return self.not_authorized(page) if not self.can_read(page): return self.not_authorized(page)
request.args = [media.file] request.args = [media.filename]
return current.response.download(request,db) return current.response.download(request,db)
else: else:
raise HTTP(404) raise HTTP(404)
@@ -4884,12 +4959,13 @@ class Wiki(object):
if query is None: if query is None:
query = (db.wiki_page.id==db.wiki_tag.wiki_page)&\ query = (db.wiki_page.id==db.wiki_tag.wiki_page)&\
(db.wiki_tag.name.belongs(tags)) (db.wiki_tag.name.belongs(tags))
query = query|db.wiki_page.title.startswith(request.vars.q) query = query|db.wiki_page.title.contains(request.vars.q)
if self.restrict_search and not self.manage(): if self.restrict_search and not self.manage():
query = query&(db.wiki_page.created_by==self.auth.user_id) query = query&(db.wiki_page.created_by==self.auth.user_id)
pages = db(query).select( pages = db(query).select(count,
*fields,**dict(orderby=orderby or ~count, *fields,**dict(orderby=orderby or ~count,
groupby=db.wiki_page.id, groupby=reduce(lambda a,b:a|b,fields),
distinct=True,
limitby=limitby)) limitby=limitby))
if request.extension in ('html','load'): if request.extension in ('html','load'):
if not pages: if not pages:
@@ -4897,18 +4973,19 @@ class Wiki(object):
_class='w2p_wiki_form')) _class='w2p_wiki_form'))
def link(t): def link(t):
return A(t,_href=URL(args='_search',vars=dict(q=t))) return A(t,_href=URL(args='_search',vars=dict(q=t)))
items = [DIV(H3(A(p.title,_href=URL(args=p.slug))), items = [DIV(H3(A(p.wiki_page.title,_href=URL(
MARKMIN(self.first_paragraph(p)) \ args=p.wiki_page.slug))),
MARKMIN(self.first_paragraph(p.wiki_page)) \
if preview else '', if preview else '',
DIV(_class='w2p_wiki_tags', DIV(_class='w2p_wiki_tags',
*[link(t.strip()) for t in \ *[link(t.strip()) for t in \
p.tags or [] if t.strip()]), p.wiki_page.tags or [] if t.strip()]),
_class='w2p_wiki_search_item') _class='w2p_wiki_search_item')
for p in pages] for p in pages]
content.append(DIV(_class='w2p_wiki_pages',*items)) content.append(DIV(_class='w2p_wiki_pages',*items))
else: else:
cloud=False cloud=False
content = [p.as_dict() for p in pages] content = [p.wiki_page.as_dict() for p in pages]
elif cloud: elif cloud:
content.append(self.cloud()['content']) content.append(self.cloud()['content'])
if request.extension=='load': if request.extension=='load':
@@ -4919,19 +4996,26 @@ class Wiki(object):
count = db.wiki_tag.wiki_page.count(distinct=True) count = db.wiki_tag.wiki_page.count(distinct=True)
ids = db(db.wiki_tag).select( ids = db(db.wiki_tag).select(
db.wiki_tag.name,count, db.wiki_tag.name,count,
groupby=db.wiki_tag.name, distinct=True,
orderby=~count,limitby=(0,20)) groupby = db.wiki_tag.name,
orderby = ~count, limitby=(0,20))
if ids: if ids:
a,b = ids[0](count), ids[-1](count) a,b = ids[0](count), ids[-1](count)
def scale(c): def style(c):
return '%.2f' % (3.0*(c-b)/max(a-b,1)+1) STYLE ='padding:0 0.2em;line-height:%.2fem;font-size:%.2fem'
items = [A(item.wiki_tag.name, size = (1.5*(c-b)/max(a-b,1)+1.3)
_style='padding:0.2em;font-size:%sem' \ return STYLE % (1.3,size)
% scale(item(count)), items = []
_href=URL(args='_search', for item in ids:
vars=dict(q=item.wiki_tag.name))) items.append(A(item.wiki_tag.name,
for item in ids] _style=style(item(count)),
return dict(content=DIV(_class='w2p_cloud',*items)) _href=URL(args='_search',
vars=dict(q=item.wiki_tag.name))))
items.append(' ')
return dict(content = DIV(_class='w2p_cloud',*items))
def preview(self, render):
request = current.request
return render(request.post_vars)
if __name__ == '__main__': if __name__ == '__main__':
import doctest import doctest
+46 -15
View File
@@ -9,6 +9,9 @@ License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
This file specifically includes utilities for security. This file specifically includes utilities for security.
""" """
import string
import threading
import struct
import hashlib import hashlib
import hmac import hmac
import uuid import uuid
@@ -18,7 +21,11 @@ import os
import re import re
import logging import logging
import socket import socket
from contrib.pbkdf2 import pbkdf2_hex try:
from contrib.pbkdf2 import pbkdf2_hex
HAVE_PBKDF2 = True
except ImportError:
HAVE_PBKDF2 = False
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
@@ -88,7 +95,7 @@ DIGEST_ALG_BY_SIZE = {
} }
### compute constant ctokens ### compute constant CTOKENS
def initialize_urandom(): def initialize_urandom():
""" """
This function and the web2py_uuid follow from the following discussion: This function and the web2py_uuid follow from the following discussion:
@@ -108,6 +115,7 @@ def initialize_urandom():
random.seed(node_id + microseconds) random.seed(node_id + microseconds)
try: try:
os.urandom(1) os.urandom(1)
have_urandom = True
try: try:
# try to add process-specific entropy # try to add process-specific entropy
frandom = open('/dev/urandom','wb') frandom = open('/dev/urandom','wb')
@@ -119,14 +127,33 @@ def initialize_urandom():
# works anyway # works anyway
pass pass
except NotImplementedError: except NotImplementedError:
have_urandom = False
logger.warning( logger.warning(
"""Cryptographically secure session management is not possible on your system because """Cryptographically secure session management is not possible on your system because
your system does not provide a cryptographically secure entropy source. your system does not provide a cryptographically secure entropy source.
This is not specific to web2py; consider deploying on a different operating system.""") This is not specific to web2py; consider deploying on a different operating system.""")
return ctokens unpacked_ctokens = struct.unpack('=QQ',string.join(
ctokens = initialize_urandom() (chr(x) for x in ctokens),''))
return unpacked_ctokens, have_urandom
UNPACKED_CTOKENS, HAVE_URANDOM = initialize_urandom()
def web2py_uuid(): def fast_urandom16(urandom=[], locker = threading.RLock()):
"""
this is 4x faster than calling os.urandom(16) and prevents
the "too many files open" issue with concurrent access to os.urandom()
"""
try:
return urandom.pop()
except IndexError:
try:
locker.acquire()
ur = os.urandom(16*1024)
urandom += [ur[i:i+16] for i in xrange(16,1024*16,16)]
return ur[0:16]
finally:
locker.release()
def web2py_uuid(ctokens=UNPACKED_CTOKENS):
""" """
This function follows from the following discussion: This function follows from the following discussion:
http://groups.google.com/group/web2py-developers/browse_thread/thread/7fd5789a7da3f09 http://groups.google.com/group/web2py-developers/browse_thread/thread/7fd5789a7da3f09
@@ -134,15 +161,17 @@ def web2py_uuid():
It works like uuid.uuid4 except that tries to use os.urandom() if possible It works like uuid.uuid4 except that tries to use os.urandom() if possible
and it XORs the output with the tokens uniquely associated with this machine. and it XORs the output with the tokens uniquely associated with this machine.
""" """
bytes = [random.randrange(256) for i in range(16)] rand_longs = (random.getrandbits(64),random.getrandbits(64))
try: if HAVE_URANDOM:
ubytes = [ord(c) for c in os.urandom(16)] # use /dev/urandom if possible urand_longs = struct.unpack('=QQ', fast_urandom16())
bytes = [bytes[i] ^ ubytes[i] for i in range(16)] byte_s = struct.pack('=QQ',
except NotImplementedError: rand_longs[0]^urand_longs[0]^ctokens[0],
pass rand_longs[1]^urand_longs[1]^ctokens[1])
## xor bytes with constant ctokens else:
bytes = ''.join(chr(c ^ ctokens[i]) for i,c in enumerate(bytes)) byte_s = struct.pack('=QQ',
return str(uuid.UUID(bytes=bytes, version=4)) rand_longs[0]^ctokens[0],
rand_longs[1]^ctokens[1])
return str(uuid.UUID(bytes=byte_s, version=4))
REGEX_IPv4 = re.compile('(\d+)\.(\d+)\.(\d+)\.(\d+)') REGEX_IPv4 = re.compile('(\d+)\.(\d+)\.(\d+)\.(\d+)')
@@ -161,6 +190,8 @@ def is_valid_ip_address(address):
elif address.lower() in ('unkown',''): elif address.lower() in ('unkown',''):
return False return False
elif address.count('.')==3: # assume IPv4 elif address.count('.')==3: # assume IPv4
if address.startswith('::ffff:'):
address = address[7:]
if hasattr(socket,'inet_aton'): # try validate using the OS if hasattr(socket,'inet_aton'): # try validate using the OS
try: try:
addr = socket.inet_aton(address) addr = socket.inet_aton(address)
@@ -169,7 +200,7 @@ def is_valid_ip_address(address):
return False return False
else: # try validate using Regex else: # try validate using Regex
match = REGEX_IPv4.match(address) match = REGEX_IPv4.match(address)
if match and all(0<=int(math.group(i))<256 for i in (1,2,3,4)): if match and all(0<=int(match.group(i))<256 for i in (1,2,3,4)):
return True return True
return False return False
elif hasattr(socket,'inet_pton'): # assume IPv6, try using the OS elif hasattr(socket,'inet_pton'): # assume IPv6, try using the OS
+119 -20
View File
@@ -117,6 +117,10 @@ class Validator(object):
""" """
return value return value
def __call__(self,value):
raise NotImplementedError
return (value, None)
class IS_MATCH(Validator): class IS_MATCH(Validator):
""" """
@@ -462,12 +466,13 @@ class IS_IN_DB(Validator):
groupby = self.groupby groupby = self.groupby
distinct = self.distinct distinct = self.distinct
dd = dict(orderby=orderby, groupby=groupby, dd = dict(orderby=orderby, groupby=groupby,
distinct=distinct, cache=self.cache) distinct=distinct, cache=self.cache,
cacheable=True)
records = self.dbset(table).select(*fields, **dd) records = self.dbset(table).select(*fields, **dd)
else: else:
orderby = self.orderby or \ orderby = self.orderby or \
reduce(lambda a,b:a|b,(f for f in fields if not f.name=='id')) reduce(lambda a,b:a|b,(f for f in fields if not f.name=='id'))
dd = dict(orderby=orderby, cache=self.cache) dd = dict(orderby=orderby, cache=self.cache, cacheable=True)
records = self.dbset(table).select(table.ALL, **dd) records = self.dbset(table).select(table.ALL, **dd)
self.theset = [str(r[self.kfield]) for r in records] self.theset = [str(r[self.kfield]) for r in records]
if isinstance(self.label,str): if isinstance(self.label,str):
@@ -488,6 +493,8 @@ class IS_IN_DB(Validator):
table = self.dbset.db[self.ktable] table = self.dbset.db[self.ktable]
field = table[self.kfield] field = table[self.kfield]
if self.multiple: if self.multiple:
if self._and:
raise NotImplementedError
if isinstance(value,list): if isinstance(value,list):
values=value values=value
elif value: elif value:
@@ -497,8 +504,20 @@ class IS_IN_DB(Validator):
if isinstance(self.multiple,(tuple,list)) and \ if isinstance(self.multiple,(tuple,list)) and \
not self.multiple[0]<=len(values)<self.multiple[1]: not self.multiple[0]<=len(values)<self.multiple[1]:
return (values, translate(self.error_message)) return (values, translate(self.error_message))
if self.dbset(field.belongs(values)).count()==len(values): if self.theset:
return (values, None) if not [v for v in values if not v in self.theset]:
return (values, None)
else:
from dal import GoogleDatastoreAdapter
def count(values, s=self.dbset, f=field):
return s(f.belongs(map(int,values))).count()
if isinstance(self.dbset.db._adapter, GoogleDatastoreAdapter):
range_ids = range(0,len(values),30)
total = sum(count(values[i:i+30]) for i in range_ids)
if total == len(values):
return (values, None)
elif count(values) == len(values):
return (values, None)
elif self.theset: elif self.theset:
if str(value) in self.theset: if str(value) in self.theset:
if self._and: if self._and:
@@ -2329,11 +2348,12 @@ class IS_LIST_OF(Validator):
new_value = [] new_value = []
if self.other: if self.other:
for item in ivalue: for item in ivalue:
(v, e) = self.other(item) if item.strip():
if e: (v, e) = self.other(item)
return (value, e) if e:
else: return (ivalue, e)
new_value.append(v) else:
new_value.append(v)
ivalue = new_value ivalue = new_value
return (ivalue, None) return (ivalue, None)
@@ -2592,7 +2612,9 @@ class LazyCrypt(object):
key = self.crypt.key key = self.crypt.key
else: else:
key = '' key = ''
if stored_password.count('$')==2: if stored_password is None:
return False
elif stored_password.count('$')==2:
(digest_alg, salt, hash) = stored_password.split('$') (digest_alg, salt, hash) = stored_password.split('$')
h = simple_hash(self.password, key, salt, digest_alg) h = simple_hash(self.password, key, salt, digest_alg)
temp_pass = '%s$%s$%s' % (digest_alg, salt, h) temp_pass = '%s$%s$%s' % (digest_alg, salt, h)
@@ -2707,6 +2729,43 @@ class CRYPT(object):
return ('', translate(self.error_message)) return ('', translate(self.error_message))
return (LazyCrypt(self,value),None) return (LazyCrypt(self,value),None)
# entropy calculator for IS_STRONG
#
lowerset = frozenset(unicode('abcdefghijklmnopqrstuvwxyz'))
upperset = frozenset(unicode('ABCDEFGHIJKLMNOPQRSTUVWXYZ'))
numberset = frozenset(unicode('0123456789'))
sym1set = frozenset(unicode('!@#$%^&*()'))
sym2set = frozenset(unicode('~`-_=+[]{}\\|;:\'",.<>?/'))
otherset = frozenset(unicode('0123456789abcdefghijklmnopqrstuvwxyz')) # anything else
def calc_entropy(string):
" calculate a simple entropy for a given string "
import math
alphabet = 0 # alphabet size
other = set()
seen = set()
lastset = None
if isinstance(string, str):
string = unicode(string, encoding='utf8')
for c in string:
# classify this character
inset = otherset
for cset in (lowerset, upperset, numberset, sym1set, sym2set):
if c in cset:
inset = cset
break
# calculate effect of character on alphabet size
if inset not in seen:
seen.add(inset)
alphabet += len(inset) # credit for a new character set
elif c not in other:
alphabet += 1 # credit for unique characters
other.add(c)
if inset is not lastset:
alphabet += 1 # credit for set transitions
lastset = cset
entropy = len(string) * math.log(alphabet) / 0.6931471805599453 # math.log(2)
return round(entropy, 2)
class IS_STRONG(object): class IS_STRONG(object):
""" """
@@ -2716,23 +2775,61 @@ class IS_STRONG(object):
requires=IS_STRONG(min=10, special=2, upper=2)) requires=IS_STRONG(min=10, special=2, upper=2))
enforces complexity requirements on a field enforces complexity requirements on a field
>>> IS_STRONG(es=True)('Abcd1234')
('Abcd1234', 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|')
>>> IS_STRONG(es=True)('Abcd1234!')
('Abcd1234!', None)
>>> IS_STRONG(es=True, entropy=1)('a')
('a', None)
>>> IS_STRONG(es=True, entropy=1, min=2)('a')
('a', 'Minimum length is 2')
>>> IS_STRONG(es=True, entropy=100)('abc123')
('abc123', 'Entropy (32.35) less than required (100)')
>>> IS_STRONG(es=True, entropy=100)('and')
('and', 'Entropy (14.57) less than required (100)')
>>> IS_STRONG(es=True, entropy=100)('aaa')
('aaa', 'Entropy (14.42) less than required (100)')
>>> IS_STRONG(es=True, entropy=100)('a1d')
('a1d', 'Entropy (15.97) less than required (100)')
>>> IS_STRONG(es=True, entropy=100)('añd')
('a\\xc3\\xb1d', 'Entropy (18.13) less than required (100)')
""" """
def __init__(self, min=8, max=20, upper=1, lower=1, number=1, def __init__(self, min=None, max=None, upper=None, lower=None, number=None,
special=1, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|', entropy=None,
invalid=' "', error_message=None): special=None, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|',
self.min = min invalid=' "', error_message=None, es=False):
self.max = max self.entropy = entropy
self.upper = upper if entropy is None:
self.lower = lower # enforce default requirements
self.number = number self.min = 8 if min is None else min
self.special = special self.max = max # was 20, but that doesn't make sense
self.upper = 1 if upper is None else upper
self.lower = 1 if lower is None else lower
self.number = 1 if number is None else number
self.special = 1 if special is None else special
else:
# by default, an entropy spec is exclusive
self.min = min
self.max = max
self.upper = upper
self.lower = lower
self.number = number
self.special = special
self.specials = specials self.specials = specials
self.invalid = invalid self.invalid = invalid
self.error_message = error_message self.error_message = error_message
self.estring = es # return error message as string (for doctest)
def __call__(self, value): def __call__(self, value):
failures = [] failures = []
if self.entropy is not None:
entropy = calc_entropy(value)
if entropy < self.entropy:
failures.append(translate("Entropy (%(have)s) less than required (%(need)s)") \
% dict(have=entropy, need=self.entropy))
if type(self.min) == int and self.min > 0: if type(self.min) == int and self.min > 0:
if not len(value) >= self.min: if not len(value) >= self.min:
failures.append(translate("Minimum length is %s") % self.min) failures.append(translate("Minimum length is %s") % self.min)
@@ -2743,7 +2840,7 @@ class IS_STRONG(object):
all_special = [ch in value for ch in self.specials] all_special = [ch in value for ch in self.specials]
if self.special > 0: if self.special > 0:
if not all_special.count(True) >= self.special: if not all_special.count(True) >= self.special:
failures.append(translate("Must include at least %s of the following : %s") \ failures.append(translate("Must include at least %s of the following: %s") \
% (self.special, self.specials)) % (self.special, self.specials))
if self.invalid: if self.invalid:
all_invalid = [ch in value for ch in self.invalid] all_invalid = [ch in value for ch in self.invalid]
@@ -2783,6 +2880,8 @@ class IS_STRONG(object):
if len(failures) == 0: if len(failures) == 0:
return (value, None) return (value, None)
if not self.error_message: if not self.error_message:
if self.estring:
return (value, '|'.join(failures))
from html import XML from html import XML
return (value, XML('<br />'.join(failures))) return (value, XML('<br />'.join(failures)))
else: else:
+32 -18
View File
@@ -804,12 +804,12 @@ def console():
default=False, default=False,
help=msg) help=msg)
parser.add_option('-N', parser.add_option('-Y',
'--no-cron', '--run-cron',
action='store_true', action='store_true',
dest='nocron', dest='runcron',
default=False, default=False,
help='do not start cron automatically') help='start the background cron process')
parser.add_option('-J', parser.add_option('-J',
'--cronjob', '--cronjob',
@@ -904,7 +904,7 @@ def console():
if options.cronjob: if options.cronjob:
global_settings.cronjob = True # tell the world global_settings.cronjob = True # tell the world
options.nocron = True # don't start cron jobs options.run = False # don't start cron jobs
options.plain = True # cronjobs use a plain shell options.plain = True # cronjobs use a plain shell
options.nobanner = True options.nobanner = True
options.nogui = True options.nogui = True
@@ -955,6 +955,19 @@ def check_existent_app(options,appname):
if os.path.isdir(os.path.join(options.folder, 'applications', appname)): if os.path.isdir(os.path.join(options.folder, 'applications', appname)):
return True return True
def get_code_for_scheduler(app, options):
if len(app) == 1 or app[1] == None:
code = "from gluon import current;current._scheduler.loop()"
else:
code = "from gluon import current;current._scheduler.group_names = ['%s'];"
code += "current._scheduler.loop()"
code = code % ("','".join(app[1:]))
app_ = app[0]
if not check_existent_app(options, app_):
print "Application '%s' doesn't exist, skipping" % (app_)
return None, None
return app_, code
def start_schedulers(options): def start_schedulers(options):
try: try:
from multiprocessing import Process from multiprocessing import Process
@@ -965,20 +978,21 @@ def start_schedulers(options):
apps = [(app.strip(), None) for app in options.scheduler.split(',')] apps = [(app.strip(), None) for app in options.scheduler.split(',')]
if options.scheduler_groups: if options.scheduler_groups:
apps = options.scheduler_groups apps = options.scheduler_groups
code = "from gluon import current;current._scheduler.loop()"
logging.getLogger().setLevel(options.debuglevel)
if len(apps) == 1 and not options.with_scheduler:
app_, code = get_code_for_scheduler(apps[0], options)
if not app_:
return
print 'starting single-scheduler for "%s"...' % app_
run(app_,True,True,None,False,code)
return
for app in apps: for app in apps:
if len(app) == 1 or app[1] == None: app_, code = get_code_for_scheduler(app, options)
code = "from gluon import current;current._scheduler.loop()" if not app_:
else:
code = "from gluon import current;current._scheduler.group_names = ['%s'];"
code += "current._scheduler.loop()"
code = code % ("','".join(app[1:]))
app_ = app[0]
if not check_existent_app(options, app_):
print "Application '%s' doesn't exist, skipping" % (app_)
continue continue
print 'starting scheduler for "%s"...' % app_ print 'starting scheduler for "%s"...' % app_
args = (app_,True,True,None,False,code) args = (app_,True,True,None,False,code)
logging.getLogger().setLevel(options.debuglevel)
p = Process(target=run, args=args) p = Process(target=run, args=args)
processes.append(p) processes.append(p)
print "Currently running %s scheduler processes" % (len(processes)) print "Currently running %s scheduler processes" % (len(processes))
@@ -1069,13 +1083,13 @@ def start(cron=True):
return return
# ## if -N or not cron disable cron in this *process* # ## if -H cron is enabled in this *process*
# ## if --softcron use softcron # ## if --softcron use softcron
# ## use hardcron in all other cases # ## use hardcron in all other cases
if cron and not options.nocron and options.softcron: if cron and options.runcron and options.softcron:
print 'Using softcron (but this is not very efficient)' print 'Using softcron (but this is not very efficient)'
global_settings.web2py_crontype = 'soft' global_settings.web2py_crontype = 'soft'
elif cron and not options.nocron: elif cron and options.runcron:
logger.debug('Starting hardcron...') logger.debug('Starting hardcron...')
global_settings.web2py_crontype = 'hard' global_settings.web2py_crontype = 'hard'
newcron.hardcron(options.folder).start() newcron.hardcron(options.folder).start()
+2 -2
View File
@@ -94,8 +94,6 @@ class Web2pyService(Service):
os.chdir(dir) os.chdir(dir)
from gluon.settings import global_settings from gluon.settings import global_settings
global_settings.gluon_parent = dir global_settings.gluon_parent = dir
from gluon.custom_import import custom_import_install
custom_import_install(dir)
return True return True
except: except:
self.log("Can't change to web2py working path; server is stopped") self.log("Can't change to web2py working path; server is stopped")
@@ -154,6 +152,8 @@ class Web2pyService(Service):
def web2py_windows_service_handler(argv=None, opt_file='options'): def web2py_windows_service_handler(argv=None, opt_file='options'):
path = os.path.dirname(__file__) path = os.path.dirname(__file__)
web2py_path = up(path) web2py_path = up(path)
if web2py_path.endswith('.zip'): # in case bianry distro 'library.zip'
web2py_path = os.path.dirname(web2py_path)
os.chdir(web2py_path) os.chdir(web2py_path)
classstring = os.path.normpath( classstring = os.path.normpath(
os.path.join(web2py_path,'gluon.winservice.Web2pyService')) os.path.join(web2py_path,'gluon.winservice.Web2pyService'))
+2 -2
View File
@@ -76,8 +76,8 @@ propagate=0
# welcome app handler # welcome app handler
[logger_welcome] [logger_welcome]
level=WARNING level=WARNING
qualname=web2py.app.welcome,rotatingFileHandler qualname=web2py.app.welcome
handlers=consoleHandler handlers=consoleHandler,rotatingFileHandler
propagate=0 propagate=0
# loggers for legacy getLogger calls: Rocket and markdown # loggers for legacy getLogger calls: Rocket and markdown
+6 -4
View File
@@ -35,11 +35,13 @@ BASE = '' # optonal prefix for incoming URLs
routes_in = ( routes_in = (
# do not reroute admin unless you want to disable it # do not reroute admin unless you want to disable it
(BASE+'/admin/?$anything','/admin/$anything'), (BASE+'/admin','/admin/default/index'),
(BASE+'/admin/$anything','/admin/$anything'),
# do not reroute appadmin unless you want to disable it # do not reroute appadmin unless you want to disable it
(BASE+'/$app/appadmin/?$anything','/$app/appadmin/$anything'), (BASE+'/$app/appadmin','/$app/appadmin/index'),
(BASE+'/$app/appadmin/$anything','/$app/appadmin/$anything'),
# do not reroute static files # do not reroute static files
(BASE+'/$app/static/?$anything','/$app/static/$anything'), (BASE+'/$app/static/$anything','/$app/static/$anything'),
# reroute favicon and robots, use exable for lack of better choice # reroute favicon and robots, use exable for lack of better choice
('/favicon.ico', '/examples/static/favicon.ico'), ('/favicon.ico', '/examples/static/favicon.ico'),
('/robots.txt', '/examples/static/robots.txt'), ('/robots.txt', '/examples/static/robots.txt'),
@@ -55,7 +57,7 @@ routes_in = (
routes_out = ( routes_out = (
# do not reroute admin unless you want to disable it # do not reroute admin unless you want to disable it
('/admin/$anything', BASE+'/admin/?$anything'), ('/admin/$anything', BASE+'/admin/$anything'),
# do not reroute appadmin unless you want to disable it # do not reroute appadmin unless you want to disable it
('/$app/appadmin/$anything',BASE+'/$app/appadmin/$anything'), ('/$app/appadmin/$anything',BASE+'/$app/appadmin/$anything'),
# do not reroute static files # do not reroute static files
-25
View File
@@ -1,25 +0,0 @@
import os
import sys
paths = [sys.argv[1]]
paths1 = []
paths2 = []
while paths:
path = paths.pop()
for filename in os.listdir(path):
fullname = os.path.join(path,filename)
if os.path.isdir(fullname):
paths.append(fullname)
else:
extension = filename.split('.')[-1]
if extension.lower() in ('png','gif','jpg','jpeg','js','css'):
paths1.append((filename,fullname))
if extension.lower() in ('css','js','py','html'):
paths2.append(fullname)
for filename,fullname in paths1:
for otherfullname in paths2:
if open(otherfullname).read().find(filename)>=0:
break
else:
print fullname
# os.system('hg rm '+fullname)
# os.system('rm '+fullname)
+1 -1
View File
@@ -23,7 +23,7 @@ applications/welcome/controllers/default.py
# files and folders to exclude from gluon folder (comment with # if needed) # files and folders to exclude from gluon folder (comment with # if needed)
IGNORED = """ IGNORED = """
gluon/contrib/comet_messaging.py gluon/contrib/websocket_messaging.py
gluon/contrib/feedparser.py gluon/contrib/feedparser.py
gluon/contrib/generics.py gluon/contrib/generics.py
gluon/contrib/gql.py gluon/contrib/gql.py
+16 -7
View File
@@ -94,11 +94,11 @@ class SessionSetDb(SessionSet):
"""Return list of SessionDb instances for existing sessions.""" """Return list of SessionDb instances for existing sessions."""
sessions = [] sessions = []
tablename = 'web2py_session' tablename = 'web2py_session'
if request.application: from gluon import current
tablename = 'web2py_session_' + request.application (record_id_name, table, record_id, unique_key) = \
if tablename in db: current.response._dbtable_and_field
for row in db(db[tablename].id > 0).select(): for row in table._db(table.id > 0).select():
sessions.append(SessionDb(row)) sessions.append(SessionDb(row))
return sessions return sessions
@@ -121,8 +121,11 @@ class SessionDb(object):
self.row = row self.row = row
def delete(self): def delete(self):
from gluon import current
(record_id_name, table, record_id, unique_key) = \
current.response._dbtable_and_field
self.row.delete_record() self.row.delete_record()
db.commit() table._db.commit()
def get(self): def get(self):
session = Storage() session = Storage()
@@ -130,7 +133,13 @@ class SessionDb(object):
return session return session
def last_visit_default(self): def last_visit_default(self):
return self.row.modified_datetime if isinstance(self.row.modified_datetime, datetime.datetime):
return self.row.modified_datetime
else:
try:
return datetime.datetime.strptime(self.row.modified_datetime, '%Y-%m-%d %H:%M:%S.%f')
except:
print 'failed to retrieve last modified time (value: %s)' % self.row.modified_datetime
def __str__(self): def __str__(self):
return self.row.unique_key return self.row.unique_key

Some files were not shown because too many files have changed in this diff Show More