Compare commits

...

372 Commits

Author SHA1 Message Date
mdipierro ba9e95a5c2 faster non forms 2015-12-28 01:50:23 -06:00
mdipierro 157146b948 form.py 2015-12-27 17:37:18 -06:00
mdipierro 82e4b7030c fixed problem with CSRF 2015-12-27 12:07:52 -06:00
mdipierro 71fba07e3a some sqlhtml renaming 2015-12-27 07:12:56 -06:00
mdipierro 2a7a4a3d04 removed print statements 2015-12-27 05:39:08 -06:00
mdipierro 999f235b75 IS_IN_DB(...,delimiter=',',auto_add=True) 2015-12-27 05:33:29 -06:00
mdipierro da22554aed allow for IS_IN_DB(db,db.thing.id,db.thing.name) 2015-12-27 02:46:23 -06:00
mdipierro 0409d6f725 R-2.13.4 2015-12-25 22:56:50 -06:00
mdipierro b6235249da fixed path issue when starting web2py 2015-12-25 22:56:13 -06:00
mdipierro fabadcd21f do not remove gluon/packages/dal/.* files 2015-12-24 09:40:08 -06:00
mdipierro 8e4ea3497b fixed issue #1138, ldap and python 2.6.x problem 2015-12-24 09:15:19 -06:00
mdipierro 4b0e1856b5 reverted 2a245d36 2015-12-24 09:04:45 -06:00
mdipierro 94841c90c3 R-2.13.3 2015-12-24 08:50:24 -06:00
mdipierro f14e5f728c fixing mess 2015-12-24 08:48:19 -06:00
mdipierro 8443c17839 sync'ed appadmins 2015-12-24 08:36:16 -06:00
mdipierro be8114127e Merge pull request #1141 from gi0baro/master
Tracking latest pyDAL changes
2015-12-24 08:30:31 -06:00
gi0baro 4eaef303ff Tracking latest pyDAL changes 2015-12-24 15:21:52 +01:00
mdipierro 319a3fc1dc Merge branch 'BuhtigithuB-fix/no-more-deprecated-has-key' 2015-12-23 23:11:55 -06:00
mdipierro 463d643e2c fmerged 2015-12-23 23:11:34 -06:00
mdipierro 0cbed12952 Merge pull request #1137 from cassiobotaro/fix_logging
fixing logging old behaviour
2015-12-23 23:08:17 -06:00
mdipierro b5994e57a4 Merge pull request #1136 from cassiobotaro/remove_25_support
update files removing 2.5 things
2015-12-23 23:07:54 -06:00
Cássio Botaro e239b975be Change to re-run AppVeyor 2015-12-23 11:19:19 -02:00
Richard Vézina 0259ea3d29 no more deprecated .has_key(...) 2015-12-22 15:39:32 -05:00
cassiobotaro db4c008de3 Minor changes 2015-12-21 15:40:25 -02:00
cassiobotaro 7921e5148a fixing logging old behaviour 2015-12-21 12:10:50 -02:00
cassiobotaro ee23eab77a update files removing 2.5 things 2015-12-21 11:55:44 -02:00
mdipierro 2344386f77 better docstring for Auth.jwt 2015-12-18 19:19:43 -06:00
mdipierro b5e12031c5 added Auth(db,jwt=dict(secret_key='secret')) and auth.allows_jwt() before auth.requires_login() 2015-12-18 19:12:41 -06:00
mdipierro 85e6840cf0 Merge branch 'master' of github.com:web2py/web2py 2015-12-18 18:09:09 -06:00
mdipierro 4c3006acb4 Merge pull request #1135 from gi0baro/master
Track latest pyDAL GAE fix by @mdipierro
2015-12-18 18:08:53 -06:00
gi0baro f8f008cab5 Track latest pyDAL GAE fix by @mdipierro 2015-12-18 12:51:17 +01:00
mdipierro 6bff8af458 CHANGELOG 2015-12-18 04:52:43 -06:00
mdipierro b67edb083e fixed compileapp problem in appveyor test (2nd attempt) 2015-12-18 04:44:03 -06:00
mdipierro 4125a97ce1 fixed compileapp problem in appveyor test 2015-12-18 04:39:51 -06:00
mdipierro 78cf55bf9a R-2.13.2 2015-12-18 04:28:16 -06:00
mdipierro 931daaff89 fixed security issue in reset password when registration_requires_authorization, thanks Giovanni Verde 2015-12-18 04:11:26 -06:00
mdipierro c6550f0adc fixed a condition that allows reset_password if a reset link is sent before a user is blocked 2015-12-18 03:40:12 -06:00
mdipierro 22c89d8dcc version 2.13.1 2015-12-17 21:19:08 -06:00
mdipierro 1636528a0f Merge pull request #1132 from gi0baro/master
Tracking pyDAL v15.12
2015-12-17 21:08:30 -06:00
gi0baro c02229d79c Tracking pyDAL v15.12 2015-12-16 21:55:23 +01:00
mdipierro acb05dbfe1 added retrieve to fabfile and fixed minor bug 2015-12-15 23:13:46 -06:00
mdipierro fda1117dd7 fixed import 2015-12-14 15:44:34 -06:00
mdipierro d1fde23182 create_user spelling fixed 2015-12-14 15:43:50 -06:00
mdipierro adf4c93860 Merge pull request #1124 from matclab/issue1123
Convert attachments to a list if necessary.
2015-12-14 15:32:07 -06:00
mdipierro 02f1903c3d Merge pull request #1127 from gi0baro/appveyor-skip
Avoid to run pyDAL adapters tests on appveyor
2015-12-14 15:31:23 -06:00
mdipierro 1137027ecc Merge pull request #1126 from nextghost/master
Parse GET vars from rewritten query string, not the original. Fixes #730
2015-12-14 15:31:10 -06:00
mdipierro 229616b9fc added fabfile form deployment and maintenance tasks 2015-12-14 15:16:00 -06:00
gi0baro a75a8cbf46 Avoid to run pyDAL adapters tests on appveyor 2015-12-13 12:34:54 +01:00
Martin Doucha 9650ff7516 Parse GET vars from rewritten query string, not the original. Fixes #730 2015-12-12 23:59:15 +01:00
Mathieu Clabaut 5b90f3f532 Convert attachments to a list if necessary.
Also corrects a typo that was apparently silenced by the bug.
This closes issue #1123
2015-12-09 14:46:05 +01:00
mdipierro 483092787b Merge pull request #1120 from dokime7/patch-10
Fix oauth2 renew token
2015-12-07 19:58:24 -06:00
Jeremie Dokime 9b17048882 Fix oauth2 renew token
When getting an oauth token, we must use code or refresh_token but not twice.
2015-12-07 11:18:59 +01:00
mdipierro 30fe7400f9 syncing latest mydal 2015-12-04 15:14:02 -06:00
mdipierro ada9353a7e removed unwanted referene to jwt in tools 2015-12-04 15:10:25 -06:00
mdipierro b0373297e0 Browser auto-fill can corrupt data while using admin database interface #1045, thanks dkleissa 2015-12-04 12:31:35 -06:00
mdipierro 2dbbef724c DIV.elements fails when searching for an attribute with an integer value #1074 2015-12-04 12:21:05 -06:00
mdipierro eb7017fd9a fixed auth.settings.register_onaccept is not firing when signing up through third-party #1081 2015-12-04 12:14:39 -06:00
mdipierro 4c039574df skip failed views, thanks Anthony 2015-12-04 12:06:46 -06:00
mdipierro ab900957fe fixed Cookie parsing stopped after first invalid cookie #1084, thanks paultuckey 2015-12-04 11:44:51 -06:00
mdipierro f960c8f6df fixed add_membership, del_membership, add_membership = IntegrityError (when auth.enable_record_versioning) #1087 2015-12-04 11:42:06 -06:00
mdipierro d2910327c0 temp fix for IS_DATETIME with timezone info & record versioning causes TypeError: can't compare offset-naive and offset-aware datetimes #1094 2015-12-04 11:31:55 -06:00
mdipierro dfd6d52192 fixed missing helper.get(attribute) 2015-12-04 11:28:39 -06:00
mdipierro 7dd8a3c853 fixed Reference to http://..../[app]/default/user/manage_users in welcome's default controller #1102 2015-12-04 11:24:00 -06:00
mdipierro 71ae754fcd fixed #1110, allow passing unicode to template render 2015-12-04 11:16:15 -06:00
mdipierro 0520770a7e fixed #1111, Validator IS_IPV4: options is_private=False not working, thanks Nbushkov 2015-12-04 11:12:44 -06:00
mdipierro 6b880fb455 fixed class concatenation 2015-12-04 10:46:20 -06:00
mdipierro dd180019a1 Merge pull request #1118 from matclab/readonly-label
Add readonly class to labels of readonly fields and set padding to 0
2015-12-04 10:43:17 -06:00
mdipierro 638f1f902a Merge pull request #1116 from timnyborg/patch-7
Fix IS_NOT_IN_DB to work with custom primarykey
2015-12-04 10:42:25 -06:00
mdipierro 73061e3bf5 Merge pull request #1114 from BuhtigithuB/Improve/ldap-contrib-self-signed-certificate
Improve/ldap contrib self signed certificate
2015-12-04 10:42:05 -06:00
mdipierro 5a18e29c2e Merge pull request #1113 from timnyborg/patch-6
Fix grid count when using groupby on MSSQL
2015-12-04 10:41:36 -06:00
mdipierro 721af77c90 Merge pull request #1108 from dokime7/patch-9
Send client_secret when get oauth2 refresh_token
2015-12-04 10:38:04 -06:00
mdipierro 2cf6797b43 Merge pull request #1106 from erilyth/master
Added XML to retrieve pickled XML data #1067
2015-12-04 10:37:50 -06:00
mdipierro 5c4145743f Merge pull request #1103 from ilvalle/w2p_target
added w2p_target attribute in form to control the output destination
2015-12-04 10:37:14 -06:00
mdipierro b4733e4617 added gluon/contrib/web2py_jwt.py, thanks Niphlod 2015-12-04 10:30:56 -06:00
mdipierro b51d217d9b french pluralizaiton rules, thanks Mathieu Clabaut 2015-12-04 10:07:38 -06:00
Mathieu Clabaut 864dbe73f2 Add readonly class to labels of readonly fields and set padding to 0
This allow for readonly fields content to be vertical aligned with their label
2015-12-04 15:29:45 +01:00
Tim Nyborg b942fc8f7a Fix IS_NOT_IN_DB to work with custom primarykey
Validator currently selects custom id properly, but then explicitly checks .id
2015-12-03 15:02:21 +00:00
Hardirc b2a65dbba4 Support for self-signed certificate LDAPS implementation 2015-12-02 14:18:09 -05:00
Hardirc d36d4d77f7 replace .has_key() by in, .has_key() is deprecated 2015-12-02 14:04:51 -05:00
Hardirc c8db6d5fb7 Improve PEP8 and readability 2015-12-02 14:02:27 -05:00
Tim Nyborg 1b77c2294a Fix grid count when using groupby on MSSQL
Any query like SELECT COUNT(*) from (SELECT COUNT(*) FROM ...) will fail with 'No column name was specified for column 1 of '_tmp'', so I'm providing an alias for the subquery's field
2015-11-30 16:09:53 +00:00
Jeremie Dokime 98a81c9fbd Send client_secret when get oauth2 refresh_token
With some oauth2 providers, the oauth client_secret is required when getting a refresh_token.
2015-11-23 10:10:00 +01:00
Batchu Venkat Vishal 4bf5a70dc0 Added XML to retrieve pickled XML data #1067 2015-11-19 15:01:23 +05:30
ilvalle d883e3d84e added w2p_target attribute in form to control the output destination 2015-11-14 12:03:20 +01:00
mdipierro db37cf6a58 Revert "Labels should get their information from the render function of the records."
This reverts commit 65c87386c1.
2015-11-12 18:25:48 -06:00
mdipierro dba5c97d51 fixed check for form tampering 2015-11-11 18:38:48 -06:00
mdipierro 948bd0c671 fixed check for form tampering 2015-11-11 18:20:37 -06:00
mdipierro 5d8ff8ba2c removed login_once_after_registration 2015-11-11 09:14:05 -06:00
mdipierro 503cd59adc auth.settings.login_once_after_registration 2015-11-11 09:03:54 -06:00
mdipierro a0bcd2287b Merge branch 'josedesoto-issue/1095' 2015-10-30 23:10:35 -05:00
mdipierro 430163f70b fixed conflict 2015-10-30 23:10:25 -05:00
mdipierro 52b59e9b71 Merge pull request #1092 from niphlod/fix/1090
fixes #1090
2015-10-30 23:08:47 -05:00
mdipierro e8f87ea274 Merge pull request #1091 from dokime7/patch-8
Add renew token by using refresh_token
2015-10-30 23:08:37 -05:00
mdipierro fb6fa0c448 Merge pull request #1088 from niphlod/fix/1083
fixes #1083
2015-10-30 23:07:58 -05:00
mdipierro 935c95ccfc Merge pull request #1086 from gi0baro/pydal-tests
Run pydal's tests inside web2py using contrib adapters
2015-10-30 23:07:37 -05:00
mdipierro e180e69467 fixed a typo, thanks James Burke 2015-10-30 23:06:00 -05:00
engeens 5c9d197f93 issue #1095. Added two-factor authentication methods and onvalidation. Fixed last attempt two-factor retry login
issue #1095. Added return user for two_factor_onvalidation
2015-10-30 15:09:51 +01:00
mdipierro e417d311e5 added link to http://www.web2pyref.com/ 2015-10-29 20:59:15 -05:00
mdipierro 199f93f262 fixed typo in tools.py, thanks James Burke 2015-10-29 20:56:40 -05:00
niphlod 64a8880c80 fixes #1090
removed timezone for IS_DATE* validators
2015-10-26 09:50:09 +01:00
Jeremie Dokime 257c514bd4 Add renew token by using refresh_token
Handle the refresh_token mechanism to renew the access_token when expire.
2015-10-25 20:48:04 +01:00
niphlod 12f848c899 fixes #1083 2015-10-19 21:50:34 +02:00
mdipierro 4de007a946 fixed possible problem with cache.action 2015-10-16 21:39:30 -05:00
gi0baro b59a93e24e Run pydal's tests inside web2py using contrib adapters 2015-10-16 14:44:41 +02:00
mdipierro bbed326c20 fixed commit error 2015-10-07 13:15:39 -05:00
mdipierro 874398c38c Merge pull request #1080 from BuhtigithuB/improve/ldap-auth-more-dry
Make ldap_auth a bit more DRY
2015-10-07 13:12:30 -05:00
mdipierro a9f8fbadae Merge pull request #1077 from leonelcamara/fix_wiki_extra
Fixes #721
2015-10-07 13:08:16 -05:00
mdipierro e62320ff9f Merge pull request #1076 from gi0baro/master
Tracking pydal 15.09
2015-10-07 13:07:54 -05:00
mdipierro b3e606295e committed changelog and removed unwanted file 2015-10-07 13:04:12 -05:00
mdipierro b8c2bd7303 fixed LazyCrypt and fixed git problem 2015-10-07 13:02:30 -05:00
mdipierro 1387b26606 fixed LazyCrypt, thanks Denes 2015-10-07 12:57:20 -05:00
Richard Vézina c6a7732d32 Don't update record when values are the same 2015-10-06 14:36:45 -04:00
Richard Vézina 0036d9c45b Make ldap_auth a bit more DRY 2015-10-06 14:30:50 -04:00
Leonel Câmara b99fb7dedf Fixes #721
Fixes a bug where auth.wiki was not respecting the extra keyword argument
2015-09-29 00:21:01 +01:00
gi0baro 344590470b Tracking pydal 15.09 2015-09-28 15:50:42 +02:00
mdipierro 2c57dc084e Merge pull request #1073 from niphlod/fix/1043
fixes #1043 , thanks @bobstjon
2015-09-27 14:40:27 -05:00
mdipierro c17ba0a020 Merge pull request #1072 from niphlod/fix/1039
fixes #1039
2015-09-27 14:40:14 -05:00
mdipierro 7d4b460e1b Merge pull request #1071 from niphlod/fix/1068
fixes #1068
2015-09-27 14:39:58 -05:00
mdipierro 6680ea8ab7 Merge pull request #1061 from nklever/master
small changes in sqlhtml.py, validator.py, contrib/spreadsheet.py
2015-09-27 14:39:47 -05:00
niphlod 353db90a64 fixes #1043 , thanks @bobstjon 2015-09-21 22:24:59 +02:00
niphlod 827e663ac4 better list: widget 2015-09-21 22:18:18 +02:00
niphlod de399691ce fixes #1039
It was a REEEAALLY good catch :-)
2015-09-21 22:09:17 +02:00
niphlod 46f081c45c fixes #1068
threw in also a better list:string widget repr for bs3

Updated also the bootswatch theme because something was wrong
2015-09-21 21:38:28 +02:00
mdipierro 0fa0dbaeea Merge branch 'master' of github.com:web2py/web2py 2015-09-20 14:07:06 -05:00
mdipierro b47511c896 token default = web2py_uuid 2015-09-20 14:07:01 -05:00
mdipierro e31318eaa8 Merge pull request #1066 from lraphael/master
fix unindented lines
2015-09-18 00:41:34 -05:00
mdipierro 72ee538883 Merge pull request #1065 from leonelcamara/pa_integration
PythonAnywhere integration
2015-09-18 00:41:20 -05:00
mdipierro b6ddc6098e Merge pull request #1060 from viniciusban/1059-response-render-uses-original-response-view
Closes #1059: get `response.view` from the environment
2015-09-18 00:40:04 -05:00
mdipierro 90854eae44 Merge pull request #1058 from niphlod/fix/735
fixes issue #735
2015-09-18 00:39:08 -05:00
mdipierro 2bceb3f95f Merge pull request #1057 from niphlod/fix/823
fixes #823
2015-09-18 00:38:55 -05:00
mdipierro 9da1e29014 Merge pull request #1056 from niphlod/fix/wiki_typo
fixes typo in wiki.
2015-09-18 00:38:38 -05:00
Raphael Lechner 39ba9dc1a9 fix unindent lines 2015-09-16 17:12:45 +02:00
Leonel Câmara 36db9719ef Deal with the corner case of already created accounts
Polished everything a bit
2015-09-15 17:24:57 +01:00
Leonel Câmara 125cbd93a0 Allow deploying to pythonanywhere from the web2py admin that you're running locally. 2015-09-08 00:51:09 +01:00
Nik Klever bc267ce17b Added Column- and Row-Headers to be more flexible with the headers of the
spreadsheet.

Added also a boolean "select" parameter for the sheet.cell function which
allows to use a HTML select tag instead of an input tag for this cell.
2015-09-05 15:01:02 +02:00
Nik Klever 65c87386c1 Labels should get their information from the render function of the records. 2015-09-05 14:57:55 +02:00
Nik Klever 2a245d36f4 If in any of the form fields are unicode strings entered as input, the
unicode characters in these strings are lost in self.vars.

This conditions sets it back to the original input.

Might be, that this should be done at another place, but it works.
2015-09-05 14:38:32 +02:00
viniciusban dcf64a661d Closes #1059: get response.view from the environment 2015-09-03 20:39:37 -03:00
niphlod 1c74afc01b fixes issue #735 2015-09-03 18:33:54 +02:00
niphlod 5dbcda9f38 fixes #823 2015-09-03 18:08:33 +02:00
niphlod ac02d52f05 fixes typo in wiki. As usual, lack of unittests made this possible.
We should really make each developer "adopt" a piece of web2py to test
and care if we don't want to write unittests.
2015-09-03 17:56:45 +02:00
mdipierro d4270373e1 fixed bug in redirect to cas service, thanks Fernando González 2015-09-01 23:07:18 -05:00
mdipierro e4b27080ca Merge pull request #1051 from ShySec/master
added HttpOnly cookies (default)
2015-08-30 20:41:23 -05:00
mdipierro 692791a518 Merge pull request #1053 from BuhtigithuB/feature/redirect-next-var-when-logged-on-page-reload
No credentials request if logged in and URL contains user/login?_next=
2015-08-30 00:58:27 -05:00
mdipierro 9190191c7a changed the default BS3 theme 2015-08-23 10:07:32 -05:00
mdipierro 7bd8f6a1a9 Merge pull request #1054 from BuhtigithuB/Improve/PEP8-gluon-tools-py
Improve PEP8 gluon/tools.py
2015-08-21 00:07:06 -05:00
mdipierro 64e115f442 Merge pull request #1050 from timnyborg/patch-5
Update simplejsonrpc.py - move default to method signature
2015-08-21 00:03:24 -05:00
Richard Vézina 61f685d225 Improve PEP8 gluon/tools.py 2015-08-20 17:16:13 -04:00
Richard Vézina c56fc2f6a0 Improve proposed enhancement #1052 2015-08-20 15:23:59 -04:00
mdipierro bb2aa29867 fixed google link to viewer in autolinks.py 2015-08-20 14:00:21 -05:00
Richard Vézina 08b6832809 No credentials request if logged in and URL contains user/login?_next= 2015-08-19 14:47:21 -04:00
kelson cbbd1246db re import required for assertRegexpMatches port 2015-08-19 11:33:47 -04:00
kelson 0a79bf3afd assertRegexpMatches requires port for python2.5 and python2.6 2015-08-19 11:30:07 -04:00
kelson db5e58e49f added HttpOnly cookies (default)
added unit tests for cookie layout, secure cookies, and HttpOnly cookies
Session.httponly_cookies=False to revert HttpOnly cookies
2015-08-19 11:25:00 -04:00
Tim Nyborg 5030d3144f Update simplejsonrpc.py
Default best placed in method signature, 
Thanks to cassiobotaro for pointing it out
2015-08-19 11:59:40 +01:00
mdipierro edcc2e44dc R-2.12.3 2015-08-18 19:15:01 -05:00
mdipierro 2cf9f26b0d fixed pydal tracking again 2015-08-18 15:13:57 -05:00
mdipierro 4b99b6fdd7 adding debounced ajax calls 2015-08-18 14:16:10 -05:00
mdipierro 622430583f Merge pull request #1047 from web2py/revert-1046-master
Revert "added default HttpOnly cookies"
2015-08-18 13:59:07 -05:00
mdipierro 89cc5a5f70 Revert "added default HttpOnly cookies" 2015-08-18 13:57:44 -05:00
mdipierro 6899154fcd reverted DAL to v15.07 2015-08-18 13:46:38 -05:00
mdipierro 47c0e461f1 Merge pull request #1046 from ShySec/master
added default HttpOnly cookies
2015-08-18 11:53:54 -05:00
mdipierro 93237837ed Merge pull request #1042 from cassiobotaro/master
Allow change quality when RESIZE
2015-08-18 11:52:52 -05:00
kelson cf20ce5fae _js_cookies => not httponly_cookies 2015-08-18 12:25:13 -04:00
mdipierro 04c86f07ef Merge pull request #1032 from dsk7/allow_requires_login_to_be_determined_dynamically
Allow to specify a function for requires_login at auth decoration.
2015-08-18 11:03:37 -05:00
mdipierro 1a12c4011b Merge pull request #1031 from mmunz/master
Repair cache status page in appadmin for welcome, admin and examples …
2015-08-18 11:02:14 -05:00
mdipierro 85bbe15758 Merge pull request #1030 from josedesoto/issue/1028
fixed issue #1028, saml2 bug
2015-08-18 11:01:46 -05:00
mdipierro 5816481a44 Merge pull request #1029 from timnyborg/patch-3
simplejsonrpc: Fix TypeError when data is None
2015-08-18 11:01:19 -05:00
kelson 2675e9d229 added default HttpOnly cookies 2015-08-18 10:23:29 -04:00
mdipierro 41498917d5 Autocomplete(...at_beginning=False) 2015-08-16 15:22:45 -05:00
Cássio Botaro 8f7acd8154 Allow change quality when RESIZE 2015-08-13 02:12:49 -03:00
mdipierro 26865421b6 email change 2015-08-11 00:32:09 -05:00
mdipierro b9ee4d4730 R-2.12.2 2015-08-09 09:27:42 -05:00
mdipierro 8fd7a27d5f fixed problem with pack all and missing cache folder 2015-08-09 09:26:57 -05:00
mdipierro 69231bdd7f R-1.12.1 2015-08-07 02:20:08 -05:00
mdipierro 55dfb9e8c4 R-1.12.1 2015-08-07 02:12:17 -05:00
mdipierro 7761219cba jquery 1.11.3, bootstrap 3.3.5 2015-08-07 02:10:27 -05:00
mdipierro e31e4e236f prettydate can do UTC, fixes #1036 2015-08-07 02:04:07 -05:00
mdipierro a43d822412 changed version for testing 2015-08-06 22:02:17 -05:00
dsk7 f94bc250eb Allow to specify a function for requires_login at auth decoration. 2015-08-02 13:21:20 +02:00
mdipierro 5775d2788d reverted apache processes=1 2015-08-01 00:24:21 -05:00
mdipierro 048f275076 fixed TLS support in ldap, thanks backseat 2015-08-01 00:21:56 -05:00
Manuel Munz 8078d4b0f3 Repair cache status page in appadmin for welcome, admin and examples apps.
This fixes how values are read. For cache.disk, we need to use the second value in the dict.
For cache.ram everything is already there in the object, no need trying to get the stats (which are not there) in the loop.
Also small fix for buttons that did not open the detailed statistics when clicked (class hidden has !important in bootstrap3).
2015-08-01 00:29:34 +02:00
Tim Nyborg 5ee8c9c930 simplejsonrpc: Fix TypeError when data is none 2015-07-27 12:07:26 +01:00
Jose de Soto 6659bc0793 fixed issue #1028, saml2 bug 2015-07-27 13:05:36 +02:00
mdipierro d7caaf04cc fixed issue #933, wiki bug 2015-07-26 14:24:53 -05:00
mdipierro e95115deb4 fixed order of confirm-password field 2015-07-26 10:18:45 -05:00
mdipierro 42c69b6343 changed indent 2015-07-20 02:26:13 -05:00
mdipierro d2347dec41 CacheRepresenter 2015-07-20 02:17:59 -05:00
mdipierro 8420020c21 caching only refault represent for references 2015-07-20 02:07:47 -05:00
mdipierro 571fc6d919 changed version 2015-07-20 01:51:22 -05:00
mdipierro 52ec228eeb Merge branch 'master' of github.com:web2py/web2py 2015-07-20 01:20:22 -05:00
mdipierro 5848d9acaa Merge pull request #1023 from dmatic/master
Script to install web2py with nginx and uwsgi on centos 7
2015-07-19 11:35:31 -05:00
Dragan Matic 3e8cbd5a0d Script to install web2py with nginx and uwsgi on centos 7 2015-07-16 13:35:27 +02:00
mdipierro e276cc2fc1 Merge pull request #1019 from gi0baro/master
Updated to pydal 15.07
2015-07-16 03:57:13 -05:00
mdipierro 39a048db61 Merge pull request #1018 from cassiobotaro/master
fix validations IS_IPV6 and IS_IPADDRESS
2015-07-16 03:57:06 -05:00
mdipierro df4b896334 models speed up 2015-07-13 07:52:38 -05:00
gi0baro 6d58845153 Using pydal 15.07 2015-07-13 14:25:43 +02:00
Dragan Matic ba1f8bf741 Merge pull request #1 from web2py/master
Update from original
2015-07-10 13:09:43 +02:00
cassiobotaro a378ab3e51 fix validations IS_IPV6 and IS_IPADDRESS 2015-07-10 01:11:16 -03:00
mdipierro 2d866647e2 Merge pull request #1017 from raj454raj/master
Small typo
2015-07-07 03:55:17 -05:00
Raj 81863d69c9 Small typo
Sorry! Can't resist when you see typo on the terminal !
2015-07-07 02:38:30 +05:30
mdipierro ee2879442f tracking last master since that works better at this time 2015-07-06 10:13:47 -05:00
mdipierro ad68d2415d possibly fixed issue #243, SQLFORM.factory(..) and DAL(None) 2015-07-06 10:04:14 -05:00
mdipierro 928de67f8d fixed DAL(None) 2015-07-06 10:01:40 -05:00
mdipierro 68296f9e65 Merge pull request #1016 from ShySec/master
fix Field.Virtual use in multi-table queries
2015-07-06 07:44:49 -05:00
kelson 7ac6edae52 fix Field.Virtual use in multi-table queries 2015-07-06 08:35:58 -04:00
mdipierro 1fc90fdb6d scripts/web2py-scheduler.conf 2015-07-06 04:46:56 -05:00
mdipierro 34a9d72cde mail.settings.server='logging:filename' 2015-07-06 04:45:14 -05:00
mdipierro 198ce939d0 fixed css of population table 2015-07-04 17:12:25 -05:00
mdipierro e31a099cb3 Merge pull request #1012 from ortgit/master
Security fix: Validate for open redirect everywhere, not just in login()
2015-07-02 06:40:12 -05:00
mdipierro cc7e10d216 Merge pull request #1009 from rserbitar/master
Fix compatibility with Tornado 4
2015-07-02 06:38:35 -05:00
mdipierro d8b68036c2 Merge pull request #1006 from kjkuan/workaround-urllib-bug
Workaround http://bugs.python.org/issue9405 on OS X.
2015-07-02 06:38:18 -05:00
pallav_fdsi f9cd7e4ef4 Open redirect attacks should be caught for all functions that use the _next variable (for example: logout()) instead of just for the login() function. 2015-07-01 18:38:43 -04:00
pallav_fdsi 896b45b838 Merge branch 'master' of https://github.com/web2py/web2py 2015-07-01 17:49:47 -04:00
mdipierro d6146c9c5d no more text shadow in buttons, was horrible 2015-07-01 12:18:10 -05:00
mdipierro b3be806244 Merge pull request #1008 from leonelcamara/pack_exe
Moved pack as exe functionality to the pack_custom.html form
2015-07-01 09:01:49 -05:00
rserbitar eac12d3a57 Fix compatibility with Tornado 4
http://stackoverflow.com/questions/24851207/tornado-403-get-warning-when-opening-websocket
2015-07-01 12:06:52 +02:00
mdipierro 2fc081bc3c no more module_installer 2015-06-30 16:12:54 -05:00
Leonel Câmara 032af7c04d Moved pack as exe functionality to the pack_custom.html form 2015-06-30 19:19:38 +01:00
mdipierro 8e63825def Merge pull request #1007 from leonelcamara/pack_exe
Allow packing an application with the windows executable
2015-06-30 12:32:05 -05:00
Leonel Câmara 5d2e5dded3 Allow packing an application with the windows executable 2015-06-30 17:51:39 +01:00
mdipierro 61e33da844 module installer 2015-06-30 11:14:47 -05:00
Jack Kuan da9dbaa5d6 Workaround http://bugs.python.org/issue9405 on OS X.
See https://groups.google.com/forum/#!topic/web2py/WSjhhDet1UM
for context.
2015-06-30 10:56:31 -04:00
mdipierro 7543c54bdb enable_tokens in welcome 2015-06-30 02:32:44 -05:00
mdipierro 00608e4f04 auth.settings.enable_tokens and header web2py_user_token 2015-06-29 13:38:54 -05:00
mdipierro cdbf48f09b fixed margin-top in welcome 2015-06-29 13:03:23 -05:00
mdipierro f39db6331a dealing with issue of accidentally redefining request/response, thanks Auden RovelleQuartz 2015-06-29 03:56:22 -05:00
mdipierro ef433da190 improvements to token logic, thanks Niphlod 2015-06-28 17:01:21 -05:00
mdipierro d2375b4187 always allow access to <app>/appadmin/manage/auth if used is admin 2015-06-28 16:41:09 -05:00
mdipierro 26d87967c5 always allow access to appadmin/manage.auth if used is admin 2015-06-28 16:40:23 -05:00
mdipierro 044b2331c3 bulk_register_enabled=False 2015-06-28 10:30:05 -05:00
mdipierro c89614ada6 more strict conditions on bulk_register 2015-06-28 10:20:33 -05:00
mdipierro f0aba167b4 _token, not token 2015-06-28 09:51:45 -05:00
mdipierro bde9562b78 api_tokens in example 2015-06-28 09:49:50 -05:00
mdipierro 9a1229470a support for api_tokens 2015-06-28 09:48:08 -05:00
mdipierro f781b9e1f5 Merge branch 'peregrinius-master' 2015-06-28 09:08:54 -05:00
mdipierro fa32b7577b fixed a bug and added support for user/bulk_register 2015-06-28 09:01:10 -05:00
mdipierro 68526a0c6d allow unsorted multiword query in grid search 2015-06-28 07:52:34 -05:00
mdipierro ad2003c618 fixed recently introduced sanitize bug 2015-06-27 00:33:16 -05:00
mdipierro c1ecf823d8 added link to new tutorial 2015-06-26 08:07:31 -05:00
mdipierro 6134f82452 fixed issuer #239, locking error on FreeBSD, thanks josejachuf 2015-06-26 06:58:07 -05:00
mdipierro fbb5a8b9bb fixed issue #968, IS_MATCH validator bug with unicode, thanks alex0834 2015-06-25 04:36:16 -05:00
mdipierro df34869d65 fixes #978, autotypes and unicode strings, thanks remcoboerma 2015-06-25 04:31:41 -05:00
mdipierro 28e6999e7d fixed issue #980, Admin application: can't access directories with space in directory name, thanks mmihaltz 2015-06-25 04:30:10 -05:00
mdipierro f4f77b0cb6 fixed issue #982, LOAD with ajax=False and args 2015-06-25 04:23:49 -05:00
mdipierro 23ddb6c3c2 fixed issue #999, gluon.sanitiizer.sanitze improvement, thanks macfiron 2015-06-25 04:19:01 -05:00
mdipierro b636a5d6e9 more companies 2015-06-25 03:26:18 -05:00
mdipierro efc392966e Merge pull request #1005 from cs09g/patch-3
change misspelling
2015-06-18 06:31:51 -05:00
mdipierro cffa59a80c Merge pull request #1004 from cs09g/patch-2
simplified conditions
2015-06-18 06:31:22 -05:00
mdipierro 82a1b9f628 Merge pull request #1003 from cs09g/patch-1
remove duplicated execution
2015-06-18 06:31:08 -05:00
mdipierro 94d2f1453d Merge pull request #1002 from cs09g/patch-5
change initialization
2015-06-18 06:30:30 -05:00
mdipierro a1875ee362 Merge pull request #1001 from cs09g/patch-3
Update appadmin.py
2015-06-18 06:29:57 -05:00
mdipierro 5f13dca712 Merge pull request #1000 from niphlod/fix/readme
after appveyor hooks have been defined...
2015-06-18 06:28:39 -05:00
Chase choi f78d423c92 change misspelling 2015-06-18 17:13:38 +09:00
Chase choi f60ae809b6 simplified conditions 2015-06-18 17:05:34 +09:00
Chase choi 34dd8af101 remove duplicated execution
is that really duplicated or missing something to add for difference?
2015-06-18 16:55:04 +09:00
Chase choi 6bf6ebab1b change initialization
ok must be True or False. 
set False is better for initialization
2015-06-18 16:40:59 +09:00
Chase choi 29bf50425b Update appadmin.py 2015-06-18 16:31:28 +09:00
niphlod 8a7612c976 after appveyor hooks have been defined... 2015-06-17 21:23:29 +02:00
mdipierro 97489fd277 Merge pull request #998 from niphlod/build/appveyor
move to codecov and enable appveyor too
2015-06-17 02:00:48 -05:00
mdipierro b86184fe58 Merge pull request #997 from niphlod/fix/bs3_grid_checkbox
fix display of checkboxes in search form of grid
2015-06-17 02:00:34 -05:00
niphlod 2ce53e9957 move to codecov and enable appveyor too 2015-06-14 21:17:52 +02:00
niphlod d61c372c95 fix display of checkboxes in search form of grid 2015-06-11 21:54:21 +02:00
mdipierro 73e176365f Merge pull request #995 from niphlod/fix/994
fixes #994
2015-06-07 21:47:28 -05:00
mdipierro 33f12d91a5 Merge pull request #992 from btreecat/master
Fixed authentication using different login methods.
2015-06-07 21:47:08 -05:00
mdipierro d0f1286f03 Merge pull request #991 from cs09g/patch-2
Update appadmin.py
2015-06-07 21:44:44 -05:00
mdipierro 04d698109e Merge pull request #990 from cs09g/patch-1
remove invalid initialize
2015-06-07 21:44:19 -05:00
mdipierro 0e9c5caf4d added request_reset_password_on... 2015-06-07 21:28:18 -05:00
niphlod 509b0a6987 fixes is_in_set repr too 2015-06-06 09:50:44 +02:00
niphlod e0074ebcac fixes #994
we were overriding default classes for specific widgets
2015-06-05 22:10:33 +02:00
Stephen Tanner 918fdf2f0c Fixed authentication using different login methods. 2015-06-02 12:24:35 -04:00
Chase choi 8e827f7a09 Update appadmin.py 2015-06-02 01:28:51 +09:00
Chase choi cf2d5b637b remove invalid initialize 2015-06-02 01:00:25 +09:00
mdipierro 236fdcfafc R-2.11.2 2015-05-30 11:30:16 -05:00
mdipierro ce0f83d00c Merge pull request #988 from gi0baro/master
Track pyDAL 15.05.29
2015-05-30 11:29:09 -05:00
gi0baro 156d771ab3 Track pyDAL 15.05.29 2015-05-29 13:45:52 -05:00
mdipierro 01474c99b0 R-2.11.1 2015-05-28 23:22:16 -05:00
mdipierro 66d15491ca Merge pull request #984 from ilvalle/minorfix
lazy request.uuid
2015-05-28 23:17:21 -05:00
mdipierro 376a27da73 Merge pull request #983 from Euphorbium/patch-1
fix cPickle not defined error
2015-05-28 23:16:33 -05:00
mdipierro 0f95c13dc7 Merge pull request #981 from gi0baro/master
Track pyDAL 15.05.26
2015-05-28 23:16:02 -05:00
peregrinius a2e7794b92 Invite user
Invite by email another user to access your application. Note, my
initial version was built on Auth.register_bare which doesn't seem to be
in this repository???
2015-05-29 15:22:36 +12:00
mdipierro 926de90ee4 fixed bug in orderby when it is a list 2015-05-28 13:59:03 -05:00
ilvalle 538f375284 lazy request.uuid 2015-05-28 18:30:06 +02:00
Juozas Masiulis 4c61c0962d fix cPickle not defined error 2015-05-28 14:29:43 +03:00
gi0baro 9b71646fc5 Track pyDAL 15.05.26 2015-05-26 14:36:04 +02:00
mdipierro 1e66fa3a93 new version number 2015-05-24 19:14:49 -05:00
mdipierro 57a8dfe034 Merge pull request #977 from austinprog/patch-1
Changed "Import about this GIT repo" Line 16, typo
2015-05-24 18:58:22 -05:00
mdipierro 77e7631740 Merge pull request #976 from niphlod/fix/ubuntu_nginx_hardening
thanks  @wmunguiam for spotting
2015-05-24 18:57:51 -05:00
mdipierro ba978d55cf Merge pull request #975 from gi0baro/master
Track pyDAL 15.05
2015-05-24 18:57:36 -05:00
mdipierro 12e8ee5c25 Merge pull request #974 from niphlod/fix/redis_cache_multiapp
redis multi-app. Thanks Lisandro for spotting it
2015-05-24 18:57:26 -05:00
Austin d293e98b43 Changed "Import about this GIT repo" Line 16, typo
My proposal is to change it to "Important reminder about this GIT Repo", as I think the "Import" part in the current one is a typo.
2015-05-24 18:13:03 -05:00
niphlod 4f316d0294 thanks @wmunguiam for spotting 2015-05-24 21:25:27 +02:00
gi0baro 81e15879d4 Track pyDAL 15.05 2015-05-23 16:37:52 +02:00
niphlod cd1d6c5af1 redis multi-app. Thanks Lisandro for spotting it
redis_cache didn't play well with multiple apps for a silly mistake.
Glad that Lisadro pointed out
2015-05-21 22:26:04 +02:00
mdipierro c7d3758c77 Merge pull request #973 from omniavx/patch-1
Update dal.py
2015-05-20 08:27:41 -05:00
mdipierro 040e52278e Merge pull request #956 from cassiobotaro/master
More changes in List (request.args)
2015-05-20 08:27:16 -05:00
mdipierro 3daf953c66 syncing maintenance again 2015-05-20 08:26:03 -05:00
mdipierro de3d722ac9 fixed import, thanks Auden RovelleQuartz 2015-05-20 08:24:33 -05:00
omniavx ff10eab373 Update dal.py
I was running my application and got this error

{
<type 'exceptions.ImportError'> cannot import name Set

Version
web2py™	Version 2.10.4-stable+timestamp.2015.04.26.15.11.54
Python	Python 2.7.3: /usr/bin/python (prefix: /usr)
Traceback
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
Traceback (most recent call last):
  File "/home/www-data/web2py/gluon/restricted.py", line 227, in restricted
    exec ccode in environment
  File "/home/www-data/web2py/applications/omniavx_cxn/controllers/valuecache.py", line 6897, in <module>
  File "/home/www-data/web2py/gluon/globals.py", line 393, in <lambda>
    self._caller = lambda f: f()
  File "/home/www-data/web2py/applications/omniavx_cxn/controllers/valuecache.py", line 6584, in browse_bacct_callback
    from plugin_PowerGrid.CallBack import CallBack
  File "/home/www-data/web2py/gluon/custom_import.py", line 95, in custom_importer
    return base_importer(pname, globals, locals, fromlist, level)
  File "/home/www-data/web2py/gluon/custom_import.py", line 134, in __call__
    result = NATIVE_IMPORTER(name, globals, locals, fromlist, level)
  File "applications/omniavx_cxn/modules/plugin_PowerGrid/CallBack.py", line 41, in <module>
    from gluon.dal import Table ,Query, Set, Rows, Row
ImportError: cannot import name Set
}

same code produced no error in earlier version of web2py



line 15 of web2py/gluon/dal.py is 
{
from pydal.objects import Row, Rows, Table, Query, Expression
}

replacing that with 
{
from pydal.objects import Row, Rows, Table, Query, Set, Expression

}

solves the problem
2015-05-20 00:11:46 -05:00
mdipierro eb4d159b37 fixed process_batch_upload 2015-05-18 23:28:17 -05:00
Cássio Botaro 5ef7a8e9a1 maintains web2py pattern 2015-05-14 12:24:58 -03:00
mdipierro 76cfba7047 Merge pull request #964 from matclab/mail-send-lazy-to-unicode
#963 : Convert subject and body to unicode before sending mail
2015-05-14 09:17:04 -05:00
mdipierro f77f307869 Merge pull request #970 from ailnlv/patch-1
Fixing https://github.com/web2py/web2py/issues/969
2015-05-14 09:16:52 -05:00
mdipierro 5ef8648929 Merge pull request #967 from bletourmy/fix/966
fixes #966 - deadlock in cache.disk
2015-05-14 09:16:31 -05:00
mdipierro ed042685ea Merge pull request #965 from niphlod/fix/962
fixes #962
2015-05-14 09:16:10 -05:00
mdipierro d09ce57f12 Merge pull request #961 from niphlod/fix/628
fixes #628
2015-05-14 09:14:06 -05:00
ailnlv 169818b275 Fixing https://github.com/web2py/web2py/issues/969
Running readline.parse_and_bind("bind ^I rl_complete") makes the letter "b" stop working on the console. This patch solves the issue and correctly enables tab completition on OSX.
2015-05-13 19:23:56 -03:00
Bernard Letourmy 4b14a87463 fixes #966 - deadlock in cache.disk 2015-05-13 09:35:22 +08:00
niphlod cadf38b4f6 fixes #962 2015-05-11 21:24:20 +02:00
Mathieu Clabaut a6226d6391 Convert subject and body to unicode before sending mail
Not doing this was raising an exception :

Traceback (most recent call last):
  File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/restricted.py, line 227, in restricted
    exec ccode in environment
  File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/applications/foundit/controllers/default.py, line 127, in <module>
  File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/globals.py, line 393, in <lambda>
    self._caller = lambda f: f()
  File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/applications/foundit/controllers/default.py, line 34, in user
    return dict(form=auth())
  File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/tools.py, line 1595, in __call__
    return getattr(self, args[0])()
  File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/tools.py, line 3272, in request_reset_password
    if self.email_reset_password(user):
  File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/tools.py, line 3296, in email_reset_password
    message=self.messages.reset_password % d):
  File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/tools.py, line 798, in send
    subject=subject, body=text, **xcc)
  File /base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/api/mail.py, line 402, in send_mail
    message.send(make_sync_call=make_sync_call)
  File /base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/api/mail.py, line 1108, in send
    message = self.ToProto()
  File /base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/api/mail.py, line 1350, in ToProto
    message = super(EmailMessage, self).ToProto()
  File /base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/api/mail.py, line 1046, in ToProto
    message.set_subject(_to_str(self.subject))
  File cpp_message.pyx, line 124, in cpp_message.SetScalarAccessors.Setter (third_party/apphosting/python/protobuf/proto1/cpp_message.cc:2229)
TypeError: <class 'gluon.languages.lazyT'> has type <class 'gluon.languages.lazyT'>, but expected one of: str, unicode
2015-05-09 18:24:09 +02:00
niphlod 5c167907eb fixes #628
response.include_files is now cleaner and easier to maintain

You can specify a tuple of (type, url) to include external assets
without extension (such as the usecase described in #628)

Added tests for include_files, that was never included in CI tests
2015-05-08 21:51:56 +02:00
mdipierro 587ff56a94 linked 15.03-maintenance again 2015-05-07 22:26:42 -05:00
mdipierro 6f91fdd833 minor refactoring in grid(orderby) 2015-05-06 08:56:56 -05:00
mdipierro 6e2f9ad043 fixed examples support 2015-05-04 11:23:44 -05:00
Cássio Botaro cdca2793e0 Maintain py2.6k compability 2015-05-04 12:55:44 -03:00
Cássio Botaro a0ee649884 Update tests
- new tests to verify old behaviour 
- test otherwise without cast and defaut
- verify if behave like a list 
- more test with call function
2015-05-04 12:49:14 -03:00
Cássio Botaro 380b491724 Return old behaviours
- Better documented List
- Otherwise not binded with cast
2015-05-04 12:41:06 -03:00
mdipierro f45bf73992 Merge branch 'master' of github.com:web2py/web2py 2015-05-03 16:42:42 -05:00
mdipierro 94461724f6 Merge pull request #954 from cassiobotaro/master
Mantain backward compatibility
2015-05-03 16:42:12 -05:00
Cássio Botaro c36c391786 More test to prove backward compatibility 2015-05-03 14:03:26 -03:00
Cássio Botaro f6db7c995f Its necessary because of default=None trick 2015-05-03 14:02:11 -03:00
Cássio Botaro ccc4b96709 Added one more test
Added one more test to avoid mistake with backward compatibility
2015-05-03 13:36:24 -03:00
Cássio Botaro 71b02e3044 Maintain backward compatibility
Little change to maintain backward compatibility, related to #590
2015-05-03 13:35:05 -03:00
mdipierro 99fb1c3010 Merge branch 'master' of github.com:web2py/web2py 2015-05-03 10:31:37 -05:00
mdipierro 20067d7b93 Merge pull request #953 from niphlod/fix/691
fixes issue #691
2015-05-03 10:31:32 -05:00
mdipierro 44eb35c617 Merge pull request #952 from niphlod/fix/734
fixes #734
2015-05-03 10:31:21 -05:00
mdipierro df03317054 Merge pull request #951 from niphlod/tests/cache_ram_and_disk
more tests, general cleanup
2015-05-03 10:31:08 -05:00
mdipierro 9d873cbd1c reverted last commit 2015-05-03 10:30:43 -05:00
mdipierro 1bb4117cbd Merge pull request #950 from cassiobotaro/master
Fix List behaviour and added new feature
2015-05-03 10:27:46 -05:00
mdipierro e834186a86 Merge pull request #950 from cassiobotaro/master
Fix List behaviour and added new feature
2015-05-03 10:23:08 -05:00
mdipierro 1394942feb removed reference to python 2.5 2015-05-03 10:09:07 -05:00
niphlod 32b9b5c799 fixes issue #691 2015-05-03 16:06:10 +02:00
niphlod 340d7b5e6f fixes #734 2015-05-03 15:51:13 +02:00
niphlod 302f56ecc1 more tests, general cleanup 2015-05-03 15:33:19 +02:00
mdipierro 9b12459a82 Merge branch 'master' of github.com:web2py/web2py 2015-05-02 23:16:12 -05:00
mdipierro 8e3925820c allow disabling of confirmation in js delete 2015-05-02 23:16:05 -05:00
mdipierro 279d71d4cd Merge pull request #936 from niphlod/enhancement/919
added newer Recaptcha2 class to deal with v2.0. Fixes #919
2015-05-02 23:07:36 -05:00
Cássio Botaro 258e2e57ae Fix import errors 2015-05-02 20:36:35 -03:00
cassiobotaro 9357d810d8 Fix List behaviour and added new feature 2015-05-02 20:24:04 -03:00
mdipierro 54b385b321 grid(user_cursor=False) by default because it is broken 2015-04-26 17:16:19 -05:00
mdipierro df039e734c 2.10.4 stable 2015-04-26 10:10:18 -05:00
mdipierro 58533954dc R-2.10.4 2015-04-26 09:07:07 -05:00
mdipierro 236dc4b943 fixed submodule tracking 2015-04-26 09:04:55 -05:00
mdipierro 6612fd1cfe told git to track the right submodule branch 2015-04-26 08:50:16 -05:00
mdipierro 520950ba74 track track 15.03-maintenance (19/04/15) 2015-04-26 08:44:45 -05:00
mdipierro e943aa9c25 minor compatibility fix in ldap_auth 2015-04-26 08:39:47 -05:00
mdipierro 756aec7206 Merge pull request #944 from stephenrauch/ldap_auth-fix-nosql
Fix ldap_auth for NoSQL databases
2015-04-26 08:29:26 -05:00
mdipierro 970e2ed35c Merge pull request #939 from smorrison/slack_webhook_support
add support for reporting web2py errors via slack.com
2015-04-26 08:29:00 -05:00
mdipierro 1388c39636 Merge pull request #935 from niphlod/docs/dal
extend underline for proper sphinx formatting
2015-04-26 08:27:48 -05:00
mdipierro 6e84737924 Merge pull request #934 from niphlod/fix/931
fixes #931 . Thanks @butsyk for spotting the bug
2015-04-26 08:27:35 -05:00
stephenrauch 0ad50630f2 Fix ldap_auth for NoSQL databases
Unroll query with join to two queries when working with DB's which don't
support joins.
2015-04-24 11:02:18 -07:00
Sean Morrison f42ee15f5f add support for reporting web2py errors via slack.com 2015-04-22 19:00:21 -05:00
niphlod 77f154a56b added newer Recaptcha2 class to deal with v2.0. Fixes #919
Improvements over the "old" v1.0
- behaves well also without javascript
- use_ssl is redundant, v2.0 works only in https mode
- ajax is not useful anymore as the newer API is a lot easier

Adjusted also the addrow() method that was missing newer formstyles.
2015-04-22 00:10:05 +02:00
niphlod 2b0bfba649 extend underline for proper sphinx formatting 2015-04-21 23:59:07 +02:00
niphlod 9f1edf267d fixes #931 . Thanks @butsyk for spotting the bug 2015-04-21 21:59:42 +02:00
mdipierro f3bda9ad02 changed version 2015-04-20 18:02:17 -05:00
mdipierro f8afc76263 Merge pull request #930 from gi0baro/master
pydal -> track 15.03-maintenance (20/04/15)
2015-04-20 10:39:29 -05:00
gi0baro 65b4aaf842 pydal -> track 15.03-maintenance (20/04/15) 2015-04-20 17:29:22 +02:00
mdipierro 1b729cfbfc Merge pull request #928 from niphlod/fix/920
small typo. Fixes #920
2015-04-19 15:32:28 -05:00
mdipierro 1aa5f30091 Merge pull request #926 from niphlod/enhancement/web2py_on_iis
added web.config to deploy web2py on IIS
2015-04-19 15:32:11 -05:00
mdipierro b17174c04c Merge pull request #925 from gi0baro/master
pydal -> track 15.03-maintenance (19/04/15)
2015-04-19 15:31:41 -05:00
niphlod ac80adc9b4 small typo. Fixes #920 2015-04-19 19:49:25 +02:00
niphlod 888fa3dfc8 added setup script 2015-04-19 19:00:59 +02:00
niphlod f3d815e84b added web.config to deploy web2py on IIS 2015-04-19 15:51:17 +02:00
gi0baro 9915fdf093 pydal -> track 15.03-maintenance (19/04/15) 2015-04-19 14:45:19 +02:00
mdipierro ef8f802df9 R-2.10.4.beta 2015-04-18 15:44:04 -05:00
pallav_fdsi 842207ab33 Merge branch 'master' of https://github.com/web2py/web2py 2015-04-04 19:51:26 -04:00
pallav_fdsi c58f29bb9c Merge branch 'master' of https://github.com/web2py/web2py 2015-01-25 15:58:39 -05:00
pallav_fdsi 0b0f82b514 Merge branch 'master' of https://github.com/web2py/web2py 2014-11-21 00:29:14 -05:00
pallav_fdsi 3ab8a7bfd6 Merge branch 'master' of https://github.com/web2py/web2py 2014-11-11 00:21:27 -05:00
pallav_fdsi c5a9d2c456 Ignore the contents of the site-packages directory 2014-11-06 10:35:49 -05:00
94 changed files with 4441 additions and 1426 deletions
+2
View File
@@ -58,3 +58,5 @@ HOWTO-web2py-devel
*.sublime-project *.sublime-project
*.sublime-workspace *.sublime-workspace
.idea/* .idea/*
site-packages/
logs/
+9 -2
View File
@@ -17,14 +17,21 @@ install:
before_script: before_script:
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --download-cache $HOME/.pip-cache unittest2; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --download-cache $HOME/.pip-cache unittest2; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache coverage; fi; - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache coverage; fi;
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache python-coveralls; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache codecov; fi
- mysql -e 'create database pydal;'
- psql -c 'create database pydal;' -U postgres
- psql -c 'create extension postgis;' -U postgres -d pydal;
- psql -c 'SHOW SERVER_VERSION' -U postgres
script: export COVERAGE_PROCESS_START=gluon/tests/coverage.ini; ./web2py.py --run_system_tests --with_coverage script: export COVERAGE_PROCESS_START=gluon/tests/coverage.ini; ./web2py.py --run_system_tests --with_coverage
after_success: after_success:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coverage combine; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coverage combine; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --config_file=gluon/tests/coverage.ini; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then codecov; fi
notifications: notifications:
email: true email: true
addons:
postgresql: "9.4"
+37
View File
@@ -1,3 +1,40 @@
## 2.13.1-2
- fixed a security issue in request_reset_password
- added fabfile.py
- fixed oauth2 renew token, thanks dokime7
- fixed add_membership, del_membership, add_membership IntegrityError (when auth.enable_record_versioning)
- allow passing unicode to template render
- allow IS_NOT_IN_DB to work with custom primarykey, thanks timmyborg
- allow HttpOnly cookies
- french pluralizaiton rules, thanks Mathieu Clabaut
- fixed bug in redirect to cas service, thanks Fernando González
- allow deploying to pythonanywhere from the web2py admin that you're running locally, thanks Leonel
- better tests
- many more bug fixes
## 2.12.1-3
- security fix: Validate for open redirect everywhere, not just in login()
- allow to pack invidual apps and selected files as packed exe files
- allow bulk user registration with default bulk_register_enabled=False
- allow unsorted multiword query in grid search
- better MongoDB support with newer pyDAL
- enable <app>/appadmin/manage/auth by default for user admin
- allow mail.settings.server='logging:filename' to log emails to a file
- better caching logic
- fixed order of confirm-password field
- TLS support in ldap
- prettydate can do UTC
- jquery 1.11.3
- bootstrap 3.3.5
- moved to codecov and enabled appveyor
- many bug fixes
## 2.11.1
- Many small but significative improvements and bug fixes
## 2.10.1-2.10.2 ## 2.10.1-2.10.2
- welcome app defaults to Bootstrap 3 - welcome app defaults to Bootstrap 3
+2 -2
View File
@@ -11,7 +11,7 @@ clean:
find ./ -name '*.rej' -exec rm -f {} \; find ./ -name '*.rej' -exec rm -f {} \;
find ./ -name '#*' -exec rm -f {} \; find ./ -name '#*' -exec rm -f {} \;
find ./ -name 'Thumbs.db' -exec rm -f {} \; find ./ -name 'Thumbs.db' -exec rm -f {} \;
find ./gluon/ -name '.*' -exec rm -f {} \; # find ./gluon/ -name '.*' -exec rm -f {} \;
find ./gluon/ -name '*class' -exec rm -f {} \; find ./gluon/ -name '*class' -exec rm -f {} \;
find ./applications/admin/ -name '.*' -exec rm -f {} \; find ./applications/admin/ -name '.*' -exec rm -f {} \;
find ./applications/examples/ -name '.*' -exec rm -f {} \; find ./applications/examples/ -name '.*' -exec rm -f {} \;
@@ -32,7 +32,7 @@ update:
echo "remember that pymysql was tweaked" echo "remember that pymysql was tweaked"
src: src:
### Use semantic versioning ### Use semantic versioning
echo 'Version 2.10.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION echo 'Version 2.13.4-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files ### rm -f all junk files
make clean make clean
### clean up baisc apps ### clean up baisc apps
+4 -3
View File
@@ -13,7 +13,7 @@ Learn more at http://web2py.com
Then edit ./app.yaml and replace "yourappname" with yourappname. Then edit ./app.yaml and replace "yourappname" with yourappname.
## Import about this GIT repo ## Important reminder about this GIT repo
An important part of web2py is the Database Abstraction Layer (DAL). In early 2015 this was decoupled into a separate code-base (PyDAL). In terms of git, it is a sub-module of the main repository. An important part of web2py is the Database Abstraction Layer (DAL). In early 2015 this was decoupled into a separate code-base (PyDAL). In terms of git, it is a sub-module of the main repository.
@@ -38,9 +38,10 @@ PyDAL uses a separate stable release cycle to the rest of web2py. PyDAL releases
## Tests ## Tests
[![Build Status](https://img.shields.io/travis/web2py/web2py.svg?style=flat-square)](https://travis-ci.org/web2py/web2py) [![Build Status](https://img.shields.io/travis/web2py/web2py/master.svg?style=flat-square&label=Travis-CI)](https://travis-ci.org/web2py/web2py)
[![MS Build Status](https://img.shields.io/appveyor/ci/web2py/web2py/master.svg?style=flat-square&label=Appveyor-CI)](https://ci.appveyor.com/project/web2py/web2py)
[![Coverage Status](https://img.shields.io/codecov/c/github/web2py/web2py.svg?style=flat-square)](https://codecov.io/github/web2py/web2py)
[![Coverage Status](https://img.shields.io/coveralls/web2py/web2py.svg?style=flat-square)](https://coveralls.io/r/web2py/web2py)
## Installation Instructions ## Installation Instructions
+1 -1
View File
@@ -1 +1 @@
Version 2.10.3-stable+timestamp.2015.04.02.16.28.49 Version 2.13.3-stable+timestamp.2015.12.24.08.08.22
+30 -31
View File
@@ -49,7 +49,8 @@ if request.function == 'manage':
auth.table_group(), auth.table_group(),
auth.table_permission()]) auth.table_permission()])
manager_role = manager_action.get('role', None) if manager_action else None manager_role = manager_action.get('role', None) if manager_action else None
auth.requires_membership(manager_role)(lambda: None)() if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
raise HTTP(403, "Not authorized")
menu = False menu = False
elif (request.application == 'admin' and not session.authorized) or \ elif (request.application == 'admin' and not session.authorized) or \
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)): (request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
@@ -80,7 +81,6 @@ if False and request.tickets_db:
def get_databases(request): def get_databases(request):
dbs = {} dbs = {}
for (key, value) in global_env.items(): for (key, value) in global_env.items():
cond = False
try: try:
cond = isinstance(value, GQLDB) cond = isinstance(value, GQLDB)
except: except:
@@ -445,30 +445,31 @@ def ccache():
gae_stats['oldest'] = GetInHMS(time.time() - gae_stats['oldest_item_age']) gae_stats['oldest'] = GetInHMS(time.time() - gae_stats['oldest_item_age'])
total.update(gae_stats) total.update(gae_stats)
else: else:
# get ram stats directly from the cache object
ram_stats = cache.ram.stats[request.application]
ram['hits'] = ram_stats['hit_total'] - ram_stats['misses']
ram['misses'] = ram_stats['misses']
try:
ram['ratio'] = ram['hits'] * 100 / ram_stats['hit_total']
except (KeyError, ZeroDivisionError):
ram['ratio'] = 0
for key, value in cache.ram.storage.iteritems(): for key, value in cache.ram.storage.iteritems():
if isinstance(value, dict): if hp:
ram['hits'] = value['hit_total'] - value['misses'] ram['bytes'] += hp.iso(value[1]).size
ram['misses'] = value['misses'] ram['objects'] += hp.iso(value[1]).count
try: ram['entries'] += 1
ram['ratio'] = ram['hits'] * 100 / value['hit_total'] if value[0] < ram['oldest']:
except (KeyError, ZeroDivisionError): ram['oldest'] = value[0]
ram['ratio'] = 0 ram['keys'].append((key, GetInHMS(time.time() - value[0])))
else:
if hp:
ram['bytes'] += hp.iso(value[1]).size
ram['objects'] += hp.iso(value[1]).count
ram['entries'] += 1
if value[0] < ram['oldest']:
ram['oldest'] = value[0]
ram['keys'].append((key, GetInHMS(time.time() - value[0])))
for key in cache.disk.storage: for key in cache.disk.storage:
value = cache.disk.storage[key] value = cache.disk.storage[key]
if isinstance(value, dict): if isinstance(value[1], dict):
disk['hits'] = value['hit_total'] - value['misses'] disk['hits'] = value[1]['hit_total'] - value[1]['misses']
disk['misses'] = value['misses'] disk['misses'] = value[1]['misses']
try: try:
disk['ratio'] = disk['hits'] * 100 / value['hit_total'] disk['ratio'] = disk['hits'] * 100 / value[1]['hit_total']
except (KeyError, ZeroDivisionError): except (KeyError, ZeroDivisionError):
disk['ratio'] = 0 disk['ratio'] = 0
else: else:
@@ -480,12 +481,12 @@ def ccache():
disk['oldest'] = value[0] disk['oldest'] = value[0]
disk['keys'].append((key, GetInHMS(time.time() - value[0]))) disk['keys'].append((key, GetInHMS(time.time() - value[0])))
total['entries'] = ram['entries'] + disk['entries'] ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
total['bytes'] = ram['bytes'] + disk['bytes'] ram_keys.remove('ratio')
total['objects'] = ram['objects'] + disk['objects'] ram_keys.remove('oldest')
total['hits'] = ram['hits'] + disk['hits'] for key in ram_keys:
total['misses'] = ram['misses'] + disk['misses'] total[key] = ram[key] + disk[key]
total['keys'] = ram['keys'] + disk['keys']
try: try:
total['ratio'] = total['hits'] * 100 / (total['hits'] + total['ratio'] = total['hits'] * 100 / (total['hits'] +
total['misses']) total['misses'])
@@ -575,11 +576,9 @@ def bg_graph_model():
meta_graphmodel = dict(group=request.application, color='#ECECEC') meta_graphmodel = dict(group=request.application, color='#ECECEC')
group = meta_graphmodel['group'].replace(' ', '') group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group): if group not in subgraphs:
subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename) subgraphs[group]['tables'].append(tablename)
else:
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext', graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename)) label=table_template(tablename))
+1 -2
View File
@@ -220,7 +220,7 @@ def list_breakpoints():
"Return a list of linenumbers for current breakpoints" "Return a list of linenumbers for current breakpoints"
breakpoints = [] breakpoints = []
ok = None ok = False
try: try:
filename = os.path.join(request.env['applications_parent'], filename = os.path.join(request.env['applications_parent'],
'applications', request.vars.filename) 'applications', request.vars.filename)
@@ -235,5 +235,4 @@ def list_breakpoints():
ok = True ok = True
except Exception, e: except Exception, e:
session.flash = str(e) session.flash = str(e)
ok = False
return response.json({'ok': ok, 'breakpoints': breakpoints}) return response.json({'ok': ok, 'breakpoints': breakpoints})
+57 -26
View File
@@ -292,9 +292,6 @@ def site():
log_progress(appname) log_progress(appname)
session.flash = T(msg, dict(appname=appname, session.flash = T(msg, dict(appname=appname,
digest=md5_hash(installed))) digest=md5_hash(installed)))
elif f and form_update.vars.overwrite:
msg = 'unable to install application "%(appname)s"'
session.flash = T(msg, dict(appname=form_update.vars.name))
else: else:
msg = 'unable to install application "%(appname)s"' msg = 'unable to install application "%(appname)s"'
session.flash = T(msg, dict(appname=form_update.vars.name)) session.flash = T(msg, dict(appname=form_update.vars.name))
@@ -370,25 +367,56 @@ def pack_plugin():
session.flash = T('internal error') session.flash = T('internal error')
redirect(URL('plugin', args=request.args)) redirect(URL('plugin', args=request.args))
def pack_exe(app, base, filenames=None):
import urllib
import zipfile
from cStringIO import StringIO
# Download latest web2py_win and open it with zipfile
download_url = 'http://www.web2py.com/examples/static/web2py_win.zip'
out = StringIO()
out.write(urllib.urlopen(download_url).read())
web2py_win = zipfile.ZipFile(out, mode='a')
# Write routes.py with the application as default
routes = u'# -*- coding: utf-8 -*-\nrouters = dict(BASE=dict(default_application="%s"))' % app
web2py_win.writestr('web2py/routes.py', routes.encode('utf-8'))
# Copy the application into the zipfile
common_root = os.path.dirname(base)
for filename in filenames:
fname = os.path.join(base, filename)
arcname = os.path.join('web2py/applications', app, filename)
web2py_win.write(fname, arcname)
web2py_win.close()
response.headers['Content-Type'] = 'application/zip'
response.headers['Content-Disposition'] = 'attachment; filename=web2py.app.%s.zip' % app
out.seek(0)
return response.stream(out)
def pack_custom(): def pack_custom():
app = get_app() app = get_app()
base = apath(app, r=request) base = apath(app, r=request)
if request.post_vars.file: if request.post_vars.file:
files = request.post_vars.file files = request.post_vars.file
files = [files] if not isinstance(files,list) else files files = [files] if not isinstance(files,list) else files
fname = 'web2py.app.%s.w2p' % app if request.post_vars.doexe is None:
try: fname = 'web2py.app.%s.w2p' % app
filename = app_pack(app, request, raise_ex=True, filenames=files) try:
except Exception, e: filename = app_pack(app, request, raise_ex=True, filenames=files)
filename = None except Exception, e:
if filename: filename = None
response.headers['Content-Type'] = 'application/w2p' if filename:
disposition = 'attachment; filename=%s' % fname response.headers['Content-Type'] = 'application/w2p'
response.headers['Content-Disposition'] = disposition disposition = 'attachment; filename=%s' % fname
return safe_read(filename, 'rb') response.headers['Content-Disposition'] = disposition
return safe_read(filename, 'rb')
else:
session.flash = T('internal error: %s', e)
redirect(URL(args=request.args))
else: else:
session.flash = T('internal error: %s', e) return pack_exe(app, base, files)
redirect(URL(args=request.args))
def ignore(fs): def ignore(fs):
return [f for f in fs if not ( return [f for f in fs if not (
f[:1] in '#' or f.endswith('~') or f.endswith('.bak'))] f[:1] in '#' or f.endswith('~') or f.endswith('.bak'))]
@@ -456,9 +484,15 @@ def cleanup():
def compile_app(): def compile_app():
app = get_app() app = get_app()
c = app_compile(app, request) c = app_compile(app, request,
skip_failed_views = (request.args(1) == 'skip_failed_views'))
if not c: if not c:
session.flash = T('application compiled') session.flash = T('application compiled')
elif isinstance(c, list):
session.flash = DIV(*[T('application compiled'), BR(), BR(),
T('WARNING: The following views could not be compiled:'), BR()] +
[CAT(BR(), view) for view in c] +
[BR(), BR(), T('DO NOT use the "Pack compiled" feature.')])
else: else:
session.flash = DIV(T('Cannot compile: there are errors in your app:'), session.flash = DIV(T('Cannot compile: there are errors in your app:'),
CODE(c)) CODE(c))
@@ -744,7 +778,7 @@ def edit():
viewlist.append(aviewpath + '.html') viewlist.append(aviewpath + '.html')
if len(viewlist): if len(viewlist):
editviewlinks = [] editviewlinks = []
for v in viewlist: for v in sorted(viewlist):
vf = os.path.split(v)[-1] vf = os.path.split(v)[-1]
vargs = "/".join([viewpath.replace(os.sep, "/"), vf]) vargs = "/".join([viewpath.replace(os.sep, "/"), vf])
editviewlinks.append(A(vf.split(".")[0], editviewlinks.append(A(vf.split(".")[0],
@@ -754,6 +788,7 @@ def edit():
if len(request.args) > 2 and request.args[1] == 'controllers': if len(request.args) > 2 and request.args[1] == 'controllers':
controller = (request.args[2])[:-3] controller = (request.args[2])[:-3]
functions = find_exposed_functions(data) functions = find_exposed_functions(data)
functions = functions and sorted(functions) or []
else: else:
(controller, functions) = (None, None) (controller, functions) = (None, None)
@@ -866,13 +901,9 @@ def resolve():
def getclass(item): def getclass(item):
""" Determine item class """ """ Determine item class """
operators = {' ':'normal', '+':'plus', '-':'minus'}
if item[0] == ' ': return operators[item[0]]
return 'normal'
if item[0] == '+':
return 'plus'
if item[0] == '-':
return 'minus'
if request.vars: if request.vars:
c = '\n'.join([item[2:].rstrip() for (i, item) in enumerate(d) if item[0] c = '\n'.join([item[2:].rstrip() for (i, item) in enumerate(d) if item[0]
@@ -1067,7 +1098,7 @@ def design():
for c in controllers: for c in controllers:
data = safe_read(apath('%s/controllers/%s' % (app, c), r=request)) data = safe_read(apath('%s/controllers/%s' % (app, c), r=request))
items = find_exposed_functions(data) items = find_exposed_functions(data)
functions[c] = items functions[c] = items and sorted(items) or []
# Get all views # Get all views
views = sorted( views = sorted(
@@ -1205,7 +1236,7 @@ def plugin():
for c in controllers: for c in controllers:
data = safe_read(apath('%s/controllers/%s' % (app, c), r=request)) data = safe_read(apath('%s/controllers/%s' % (app, c), r=request))
items = find_exposed_functions(data) items = find_exposed_functions(data)
functions[c] = items functions[c] = items and sorted(items) or []
# Get all views # Get all views
views = sorted( views = sorted(
@@ -1509,7 +1540,7 @@ def upload_file():
if filename: if filename:
d = dict(filename=filename[len(path):]) d = dict(filename=filename[len(path):])
else: else:
d = dict(filename='unkown') d = dict(filename='unknown')
session.flash = T('cannot upload file "%(filename)s"', d) session.flash = T('cannot upload file "%(filename)s"', d)
redirect(request.vars.sender) redirect(request.vars.sender)
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
import base64
import os
import re
import gzip
import tarfile
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from xmlrpclib import ProtocolError
from gluon.contrib.simplejsonrpc import ServerProxy
def deploy():
response.title = T('Deploy to pythonanywhere')
return {}
def create_account():
""" Create a PythonAnywhere account """
if not request.vars:
raise HTTP(400)
if request.vars.username and request.vars.web2py_admin_password:
# Check if web2py is already there otherwise we get an error 500 too.
client = ServerProxy('https://%(username)s:%(web2py_admin_password)s@%(username)s.pythonanywhere.com/admin/webservices/call/jsonrpc' % request.vars)
try:
if client.login() is True:
return response.json({'status': 'ok'})
except ProtocolError as error:
pass
import urllib, urllib2
url = 'https://www.pythonanywhere.com/api/web2py/create_account'
data = urllib.urlencode(request.vars)
req = urllib2.Request(url, data)
try:
reply = urllib2.urlopen(req)
except urllib2.HTTPError as error:
if error.code == 400:
reply = error
elif error.code == 500:
return response.json({'status':'error', 'errors':{'username': ['An App other than web2py is installed in the domain %(username)s.pythonanywhere.com' % request.vars]}})
else:
raise
response.headers['Content-Type'] = 'application/json'
return reply.read()
def list_apps():
""" Get a list of apps both remote and local """
if not request.vars.username or not request.vars.password:
raise HTTP(400)
client = ServerProxy('https://%(username)s:%(password)s@%(username)s.pythonanywhere.com/admin/webservices/call/jsonrpc' % request.vars)
regex = re.compile('^\w+$')
local = [f for f in os.listdir(apath(r=request)) if regex.match(f)]
try:
pythonanywhere = client.list_apps()
except ProtocolError as error:
raise HTTP(error.errcode)
return response.json({'local': local, 'pythonanywhere': pythonanywhere})
def bulk_install():
""" Install a list of apps """
def b64pack(app):
"""
Given an app's name, return the base64 representation of its packed version.
"""
folder = apath(app, r=request)
tmpfile = StringIO()
tar = tarfile.TarFile(fileobj=tmpfile, mode='w')
try:
filenames = listdir(folder, '^[\w\.\-]+$', add_dirs=True,
exclude_content_from=['cache', 'sessions', 'errors'])
for fname in filenames:
tar.add(os.path.join(folder, fname), fname, False)
finally:
tar.close()
tmpfile.seek(0)
gzfile = StringIO()
w2pfp = gzip.GzipFile(fileobj=gzfile, mode='wb')
w2pfp.write(tmpfile.read())
w2pfp.close()
gzfile.seek(0)
return base64.b64encode(gzfile.read())
request.vars.apps = request.vars['apps[]']
if not request.vars.apps or not request.vars.username or not request.vars.password:
raise HTTP(400)
if not isinstance(request.vars.apps, list):
request.vars.apps = [request.vars.apps] # Only one app selected
client = ServerProxy('https://%(username)s:%(password)s@%(username)s.pythonanywhere.com/admin/webservices/call/jsonrpc' % request.vars)
for app in request.vars.apps:
try:
client.install(app, app+'.w2p', b64pack(app))
except ProtocolError as error:
raise HTTP(error.errcode)
return response.json({'status': 'ok'})
+33 -2
View File
@@ -17,6 +17,7 @@
'A new version of web2py is available: %s': 'Está disponível uma nova versão do web2py: %s', 'A new version of web2py is available: %s': 'Está disponível uma nova versão do web2py: %s',
'About': 'sobre', 'About': 'sobre',
'About application': 'Sobre a aplicação', 'About application': 'Sobre a aplicação',
'Accept Terms': 'Accept Terms',
'additional code for your application': 'código adicional para sua aplicação', 'additional code for your application': 'código adicional para sua aplicação',
'Additional code for your application': 'Código adicional para a sua aplicação', 'Additional code for your application': 'Código adicional para a sua aplicação',
'admin disabled because no admin password': ' admin desabilitado por falta de senha definida', 'admin disabled because no admin password': ' admin desabilitado por falta de senha definida',
@@ -50,6 +51,7 @@
'Autocomplete Python Code': 'Autocompletar Código Python', 'Autocomplete Python Code': 'Autocompletar Código Python',
'Available databases and tables': 'Bancos de dados e tabelas disponíveis', 'Available databases and tables': 'Bancos de dados e tabelas disponíveis',
'back': 'voltar', 'back': 'voltar',
'Begin': 'Begin',
'browse': 'buscar', 'browse': 'buscar',
'cache': 'cache', 'cache': 'cache',
'cache, errors and sessions cleaned': 'cache, erros e sessões eliminadas', 'cache, errors and sessions cleaned': 'cache, erros e sessões eliminadas',
@@ -112,11 +114,15 @@
'Deploy': 'publicar', 'Deploy': 'publicar',
'Deploy on Google App Engine': 'Publicar no Google App Engine', 'Deploy on Google App Engine': 'Publicar no Google App Engine',
'Deploy to OpenShift': 'Deploy to OpenShift', 'Deploy to OpenShift': 'Deploy to OpenShift',
'Deploy to pythonanywhere': 'Deploy to pythonanywhere',
'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere',
'Deployment Interface': 'Deployment Interface',
'Description': 'Descrição', 'Description': 'Descrição',
'design': 'modificar', 'design': 'modificar',
'DESIGN': 'Projeto', 'DESIGN': 'Projeto',
'Design for': 'Projeto de', 'Design for': 'Projeto de',
'Detailed traceback description': 'Detailed traceback description', 'Detailed traceback description': 'Detailed traceback description',
'details': 'details',
'direction: ltr': 'direção: ltr', 'direction: ltr': 'direção: ltr',
'Disable': 'Disable', 'Disable': 'Disable',
'docs': 'docs', 'docs': 'docs',
@@ -137,6 +143,7 @@
'Editing file': 'Editando arquivo', 'Editing file': 'Editando arquivo',
'Editing file "%s"': 'Editando arquivo "%s"', 'Editing file "%s"': 'Editando arquivo "%s"',
'Editing Language file': 'Editando arquivo de linguagem', 'Editing Language file': 'Editando arquivo de linguagem',
'Email Address': 'Email Address',
'Enterprise Web Framework': 'Framework web empresarial', 'Enterprise Web Framework': 'Framework web empresarial',
'Error': 'Erro', 'Error': 'Erro',
'Error logs for "%(app)s"': 'Logs de erro para "%(app)s"', 'Error logs for "%(app)s"': 'Logs de erro para "%(app)s"',
@@ -148,6 +155,7 @@
'Expand Abbreviation (html files only)': 'Expandir Abreviação (só para ficheiros html)', 'Expand Abbreviation (html files only)': 'Expandir Abreviação (só para ficheiros html)',
'export as csv file': 'exportar como arquivo CSV', 'export as csv file': 'exportar como arquivo CSV',
'exposes': 'expõe', 'exposes': 'expõe',
'exposes:': 'exposes:',
'extends': 'estende', 'extends': 'estende',
'failed to reload module': 'Falha ao recarregar o módulo', 'failed to reload module': 'Falha ao recarregar o módulo',
'failed to reload module because:': 'falha ao recarregar o módulo por:', 'failed to reload module because:': 'falha ao recarregar o módulo por:',
@@ -165,6 +173,7 @@
'Find Next': 'Localizar Seguinte', 'Find Next': 'Localizar Seguinte',
'Find Previous': 'Localizar Anterior', 'Find Previous': 'Localizar Anterior',
'First name': 'Nome', 'First name': 'Nome',
'Form has errors': 'Form has errors',
'Frames': 'Frames', 'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'Funções sem doctests resultarão em testes [aceitos].', 'Functions with no doctests will result in [passed] tests.': 'Funções sem doctests resultarão em testes [aceitos].',
'graph model': 'graph model', 'graph model': 'graph model',
@@ -174,6 +183,7 @@
'Hide/Show Translated strings': '', 'Hide/Show Translated strings': '',
'htmledit': 'htmledit', 'htmledit': 'htmledit',
'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.': 'Se o relatório acima contém um número de ticket, isso indica uma falha no controlador em execução, antes de tantar executar os doctests. Isto acontece geralmente por erro de endentação ou erro fora do código da função.\r\nO titulo em verde indica que os testes (se definidos) passaram. Neste caso os testes não são mostrados.', '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.': 'Se o relatório acima contém um número de ticket, isso indica uma falha no controlador em execução, antes de tantar executar os doctests. Isto acontece geralmente por erro de endentação ou erro fora do código da função.\r\nO titulo em verde indica que os testes (se definidos) passaram. Neste caso os testes não são mostrados.',
'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.',
'Import/Export': 'Importar/Exportar', 'Import/Export': 'Importar/Exportar',
'includes': 'inclui', 'includes': 'inclui',
'insert new': 'inserir novo', 'insert new': 'inserir novo',
@@ -198,22 +208,28 @@
'Last name': 'Sobrenome', 'Last name': 'Sobrenome',
'Last saved on:': 'Salvo em:', 'Last saved on:': 'Salvo em:',
'License for': 'Licença para', 'License for': 'Licença para',
'lists by ticket': 'lists by ticket',
'Loading...': 'Loading...',
'loading...': 'carregando...', 'loading...': 'carregando...',
'Local Apps': 'Local Apps',
'locals': 'locals', 'locals': 'locals',
'Login': 'Entrar', 'Login': 'Entrar',
'login': 'inicio de sessão', 'login': 'inicio de sessão',
'Login successful': 'Login successful',
'Login to the Administrative Interface': 'Entrar na interface adminitrativa', 'Login to the Administrative Interface': 'Entrar na interface adminitrativa',
'Login/Register': 'Login/Register',
'Logout': 'finalizar sessão', 'Logout': 'finalizar sessão',
'Lost Password': 'Senha perdida', 'Lost Password': 'Senha perdida',
'Manage': 'Manage',
'manage': 'gerenciar', 'manage': 'gerenciar',
'Manage': 'Manage',
'merge': 'juntar', 'merge': 'juntar',
'Models': 'Modelos',
'models': 'modelos', 'models': 'modelos',
'Models': 'Modelos',
'Modules': 'Módulos', 'Modules': 'Módulos',
'modules': 'módulos', 'modules': 'módulos',
'Name': 'Nome', 'Name': 'Nome',
'new application "%s" created': 'nova aplicação "%s" criada', 'new application "%s" created': 'nova aplicação "%s" criada',
'New Application Wizard': 'New Application Wizard',
'New application wizard': 'Assistente para novas aplicações ', 'New application wizard': 'Assistente para novas aplicações ',
'new plugin installed': 'novo plugin instalado', 'new plugin installed': 'novo plugin instalado',
'New Record': 'Novo registro', 'New Record': 'Novo registro',
@@ -224,6 +240,7 @@
'No databases in this application': 'Não existem bancos de dados nesta aplicação', 'No databases in this application': 'Não existem bancos de dados nesta aplicação',
'no match': 'não encontrado', 'no match': 'não encontrado',
'no package selected': 'nenhum pacote selecionado', 'no package selected': 'nenhum pacote selecionado',
'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder',
'online designer': 'online designer', 'online designer': 'online designer',
'or alternatively': 'or alternatively', 'or alternatively': 'or alternatively',
'Or Get from URL:': 'Ou Obtenha do URL:', 'Or Get from URL:': 'Ou Obtenha do URL:',
@@ -241,6 +258,7 @@
'Password': 'Senha', 'Password': 'Senha',
'password changed': 'senha alterada', 'password changed': 'senha alterada',
'Peeking at file': 'Visualizando arquivo', 'Peeking at file': 'Visualizando arquivo',
'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...',
'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" eliminado', 'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" eliminado',
'Plugin "%s" in application': 'Plugin "%s" na aplicação', 'Plugin "%s" in application': 'Plugin "%s" na aplicação',
'plugins': 'plugins', 'plugins': 'plugins',
@@ -250,8 +268,11 @@
'previous 100 rows': '100 registros anteriores', 'previous 100 rows': '100 registros anteriores',
'Private files': 'Private files', 'Private files': 'Private files',
'private files': 'private files', 'private files': 'private files',
'PythonAnywhere Apps': 'PythonAnywhere Apps',
'PythonAnywhere Password': 'PythonAnywhere Password',
'Query:': 'Consulta:', 'Query:': 'Consulta:',
'Rapid Search': 'Rapid Search', 'Rapid Search': 'Rapid Search',
'Read': 'Read',
'record': 'registro', 'record': 'registro',
'record does not exist': 'o registro não existe', 'record does not exist': 'o registro não existe',
'record id': 'id do registro', 'record id': 'id do registro',
@@ -263,6 +284,7 @@
'Replace': 'Substituir', 'Replace': 'Substituir',
'Replace All': 'Substituir Tudo', 'Replace All': 'Substituir Tudo',
'request': 'request', 'request': 'request',
'requires python-git, but not installed': 'requires python-git, but not installed',
'Resolve Conflict file': 'Arquivo de resolução de conflito', 'Resolve Conflict file': 'Arquivo de resolução de conflito',
'response': 'response', 'response': 'response',
'restore': 'restaurar', 'restore': 'restaurar',
@@ -285,6 +307,9 @@
'shell': 'Terminal', 'shell': 'Terminal',
'Site': 'site', 'Site': 'site',
'some files could not be removed': 'alguns arquicos não puderam ser removidos', 'some files could not be removed': 'alguns arquicos não puderam ser removidos',
'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying',
'source : filesystem': 'source : filesystem',
'Start a new app': 'Start a new app',
'Start searching': 'Start searching', 'Start searching': 'Start searching',
'Start wizard': 'iniciar assistente', 'Start wizard': 'iniciar assistente',
'state': 'estado', 'state': 'estado',
@@ -294,6 +319,7 @@
'Submit': 'Submit', 'Submit': 'Submit',
'submit': 'enviar', 'submit': 'enviar',
'Sure you want to delete this object?': 'Tem certeza que deseja apaagr este objeto?', 'Sure you want to delete this object?': 'Tem certeza que deseja apaagr este objeto?',
'switch to : db': 'switch to : db',
'table': 'tabela', 'table': 'tabela',
'Table name': 'Nome da tabela', 'Table name': 'Nome da tabela',
'test': 'testar', 'test': 'testar',
@@ -348,6 +374,7 @@
'update': 'atualizar', 'update': 'atualizar',
'update all languages': 'atualizar todas as linguagens', 'update all languages': 'atualizar todas as linguagens',
'Update:': 'Atualizar:', 'Update:': 'Atualizar:',
'upgrade now to %s': 'upgrade now to %s',
'upgrade web2py now': 'atualize o web2py agora', 'upgrade web2py now': 'atualize o web2py agora',
'upload': 'upload', 'upload': 'upload',
'Upload': 'Upload', 'Upload': 'Upload',
@@ -361,6 +388,7 @@
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, y ~(...) para NOT, para criar consultas mais complexas.', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, y ~(...) para NOT, para criar consultas mais complexas.',
'Use an url:': 'Use uma url:', 'Use an url:': 'Use uma url:',
'User ID': 'ID do Usuario', 'User ID': 'ID do Usuario',
'Username': 'Username',
'variables': 'variáveis', 'variables': 'variáveis',
'Version': 'Versão', 'Version': 'Versão',
'versioning': 'versionamento', 'versioning': 'versionamento',
@@ -368,10 +396,13 @@
'view': 'visão', 'view': 'visão',
'Views': 'Visões', 'Views': 'Visões',
'views': 'visões', 'views': 'visões',
'Warning!': 'Warning!',
'Web Framework': 'Web Framework', 'Web Framework': 'Web Framework',
'web2py Admin Password': 'web2py Admin Password',
'web2py is up to date': 'web2py está atualizado', 'web2py is up to date': 'web2py está atualizado',
'web2py Recent Tweets': 'Tweets Recentes de @web2py', 'web2py Recent Tweets': 'Tweets Recentes de @web2py',
'web2py upgraded; please restart it': 'web2py atualizado; favor reiniciar', 'web2py upgraded; please restart it': 'web2py atualizado; favor reiniciar',
'Welcome to web2py': 'Bem-vindo ao web2py', 'Welcome to web2py': 'Bem-vindo ao web2py',
'YES': 'SIM', 'YES': 'SIM',
'You only need these if you have already registered': 'You only need these if you have already registered',
} }
File diff suppressed because one or more lines are too long
+39 -29
View File
@@ -75,8 +75,8 @@
* this over and over... all will be bound to the document * this over and over... all will be bound to the document
*/ */
/*adds btn class to buttons*/ /*adds btn class to buttons*/
$('button', target).addClass('btn'); $('button:not([class^="btn"])', target).addClass('btn');
$('form input[type="submit"], form input[type="button"]', target).addClass('btn'); $('form input[type="submit"]:not([class^="btn"]), form input[type="button"]:not([class^="btn"])', target).addClass('btn');
/* javascript for PasswordWidget*/ /* javascript for PasswordWidget*/
$('input[type=password][data-w2p_entropy]', target).each(function() { $('input[type=password][data-w2p_entropy]', target).each(function() {
web2py.validate_entropy($(this)); web2py.validate_entropy($(this));
@@ -133,7 +133,7 @@
}, },
ajax_init: function(target) { ajax_init: function(target) {
/*called whenever a fragment gets loaded */ /*called whenever a fragment gets loaded */
$('.hidden', target).hide(); $('.w2p_hidden', target).hide();
web2py.manage_errors(target); web2py.manage_errors(target);
web2py.ajax_fields(target); web2py.ajax_fields(target);
web2py.show_if_handler(target); web2py.show_if_handler(target);
@@ -161,7 +161,7 @@
* and require no dom manipulations * and require no dom manipulations
*/ */
var doc = $(document); var doc = $(document);
doc.on('click', '.flash', function(e) { doc.on('click', '.w2p_flash', function(e) {
var t = $(this); var t = $(this);
if(t.css('top') == '0px') t.slideUp('slow'); if(t.css('top') == '0px') t.slideUp('slow');
else t.fadeOut(); else t.fadeOut();
@@ -241,7 +241,7 @@
/*personally I don't like it. /*personally I don't like it.
*if there's an error it it flashed and can be removed *if there's an error it it flashed and can be removed
*as any other message *as any other message
*doc.off('click', '.flash') *doc.off('click', '.w2p_flash')
*/ */
switch(xhr.status) { switch(xhr.status) {
case 500: case 500:
@@ -257,12 +257,20 @@
if(form.hasClass('no_trap')) { if(form.hasClass('no_trap')) {
return; return;
} }
form.attr('data-w2p_target', target);
var w2p_target = $(this).attr('data-w2p_target');
if (typeof w2p_target === typeof undefined || w2p_target === false) {
form.attr('data-w2p_target', target);
} else {
target = w2p_target;
}
var url = form.attr('action'); var url = form.attr('action');
if((url === "") || (url === "#") || (typeof url === 'undefined')) { if((url === "") || (url === "#") || (typeof url === 'undefined')) {
/* form has no action. Use component url. */ /* form has no action. Use component url. */
url = action; url = action;
} }
form.submit(function(e) { form.submit(function(e) {
web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.disableElement(form.find(web2py.formInputClickSelector));
web2py.hide_flash(); web2py.hide_flash();
@@ -490,12 +498,12 @@
* and prevent clicking on it */ * and prevent clicking on it */
disableElement: function(el) { disableElement: function(el) {
el.addClass('disabled'); el.addClass('disabled');
var method = el.is('button') ? 'html' : 'val'; var method = el.is('input') ? 'val' : 'html';
//method = el.attr('name') ? 'html' : 'val'; //method = el.attr('name') ? 'html' : 'val';
var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state if not already disabled */ /*store enabled state if not already disabled */
if(el.data('w2p:enable-with') === undefined) { if(el.data('w2p_enable_with') === undefined) {
el.data('w2p:enable-with', el[method]()); el.data('w2p_enable_with', el[method]());
} }
/*if you don't want to see "working..." on buttons, replace the following /*if you don't want to see "working..." on buttons, replace the following
* two lines with this one * two lines with this one
@@ -515,11 +523,11 @@
/* restore element to its original state which was disabled by 'disableElement' above*/ /* restore element to its original state which was disabled by 'disableElement' above*/
enableElement: function(el) { enableElement: function(el) {
var method = el.is('button') ? 'val' : 'html'; var method = el.is('input') ? 'val' : 'html';
if(el.data('w2p:enable-with') !== undefined) { if(el.data('w2p_enable_with') !== undefined) {
/* set to old enabled state */ /* set to old enabled state */
el[method](el.data('w2p:enable-with')); el[method](el.data('w2p_enable_with'));
el.removeData('w2p:enable-with'); el.removeData('w2p_enable_with');
} }
el.removeClass('disabled'); el.removeClass('disabled');
el.unbind('click.w2pDisable'); el.unbind('click.w2pDisable');
@@ -530,13 +538,13 @@
}, },
/*helper for flash messages*/ /*helper for flash messages*/
flash: function(message, status) { flash: function(message, status) {
var flash = $('.flash'); var flash = $('.w2p_flash');
web2py.hide_flash(); web2py.hide_flash();
flash.html(message).addClass(status); flash.html(message).addClass(status);
if(flash.html()) flash.append('<span id="closeflash"> &times; </span>').slideDown(); if(flash.html()) flash.append('<span id="closeflash"> &times; </span>').slideDown();
}, },
hide_flash: function() { hide_flash: function() {
$('.flash').fadeOut(0).html(''); $('.w2p_flash').fadeOut(0).html('');
}, },
show_if_handler: function(target) { show_if_handler: function(target) {
var triggers = {}; var triggers = {};
@@ -586,12 +594,14 @@
if(pre_call != undefined) { if(pre_call != undefined) {
eval(pre_call); eval(pre_call);
} }
if(confirm_message != undefined) { if(confirm_message) {
if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; if(confirm_message == 'default')
if(!web2py.confirm(confirm_message)) { confirm_message = w2p_ajax_confirm_message ||
web2py.stopEverything(e); 'Are you sure you want to delete this object?';
return; if(!web2py.confirm(confirm_message)) {
} web2py.stopEverything(e);
return;
}
} }
if(target == undefined) { if(target == undefined) {
if(method == 'GET') { if(method == 'GET') {
@@ -634,7 +644,7 @@
}); });
}, },
/* Disables form elements: /* Disables form elements:
- Caches element value in 'w2p:enable-with' data store - Caches element value in 'w2p_enable_with' data store
- Replaces element text with value of 'data-disable-with' attribute - Replaces element text with value of 'data-disable-with' attribute
- Sets disabled property to true - Sets disabled property to true
*/ */
@@ -646,8 +656,8 @@
if(disable_with == undefined) { if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]()) element.data('w2p_disable_with', element[method]())
} }
if(element.data('w2p:enable-with') === undefined) { if(element.data('w2p_enable_with') === undefined) {
element.data('w2p:enable-with', element[method]()); element.data('w2p_enable_with', element[method]());
} }
element[method](element.data('w2p_disable_with')); element[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
@@ -655,16 +665,16 @@
}, },
/* Re-enables disabled form elements: /* Re-enables disabled form elements:
- Replaces element text with cached value from 'w2p:enable-with' data store (created in `disableFormElements`) - Replaces element text with cached value from 'w2p_enable_with' data store (created in `disableFormElements`)
- Sets disabled property to false - Sets disabled property to false
*/ */
enableFormElements: function(form) { enableFormElements: function(form) {
form.find(web2py.enableSelector).each(function() { form.find(web2py.enableSelector).each(function() {
var element = $(this), var element = $(this),
method = element.is('button') ? 'html' : 'val'; method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) { if(element.data('w2p_enable_with')) {
element[method](element.data('w2p:enable-with')); element[method](element.data('w2p_enable_with'));
element.removeData('w2p:enable-with'); element.removeData('w2p_enable_with');
} }
element.prop('disabled', false); element.prop('disabled', false);
}); });
@@ -690,7 +700,7 @@
$.web2py.component_handler(target); $.web2py.component_handler(target);
}, },
main_hook: function() { main_hook: function() {
var flash = $('.flash'); var flash = $('.w2p_flash');
flash.hide(); flash.hide();
if(flash.html()) web2py.flash(flash.html()); if(flash.html()) web2py.flash(flash.html());
web2py.ajax_init(document); web2py.ajax_init(document);
+9 -9
View File
@@ -137,9 +137,9 @@
<h4>{{=T("Overview")}}</h4> <h4>{{=T("Overview")}}</h4>
<p>{{=T.M("Number of entries: **%s**", total['entries'])}}</p> <p>{{=T.M("Number of entries: **%s**", total['entries'])}}</p>
{{if total['entries'] > 0:}} {{if total['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=total['ratio'], hits=total['hits'], misses=total['misses']))}} dict( ratio=total['ratio'], hits=total['hits'], misses=total['misses']))}}
</p> </p>
<p> <p>
{{=T("Size of cache:")}} {{=T("Size of cache:")}}
{{if object_stats:}} {{if object_stats:}}
@@ -155,8 +155,8 @@
{{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.", {{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}} dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}}
</p> </p>
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle();')}} {{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "w2p_hidden" );')}}
<div class="hidden" id="all_keys"> <div class="w2p_hidden" id="all_keys">
{{=total['keys']}} {{=total['keys']}}
</div> </div>
<br /> <br />
@@ -183,8 +183,8 @@
{{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.", {{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}} dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}}
</p> </p>
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle();')}} {{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "w2p_hidden" );')}}
<div class="hidden" id="ram_keys"> <div class="w2p_hidden" id="ram_keys">
{{=ram['keys']}} {{=ram['keys']}}
</div> </div>
<br /> <br />
@@ -212,8 +212,8 @@
{{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.", {{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}} dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}}
</p> </p>
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle();')}} {{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "w2p_hidden" );')}}
<div class="hidden" id="disk_keys"> <div class="w2p_hidden" id="disk_keys">
{{=disk['keys']}} {{=disk['keys']}}
</div> </div>
<br /> <br />
+4 -2
View File
@@ -1,5 +1,7 @@
{{extend 'layout.html'}} {{extend 'layout.html'}}
{{ {{
import re
regex_space = re.compile('\s+')
def all(items): def all(items):
return reduce(lambda a,b:a and b,items,True) return reduce(lambda a,b:a and b,items,True)
def peekfile(path,file,vars={},title=None): def peekfile(path,file,vars={},title=None):
@@ -205,7 +207,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
{{=peekfile('views',c, dict(id=id))}} {{=peekfile('views',c, dict(id=id))}}
</span> </span>
<span class="extras celled celled-one"> <span class="extras celled celled-one">
{{if extend.has_key(c):}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}} {{if c in extend:}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
{{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}} {{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}}
</span> </span>
</li> </li>
@@ -304,7 +306,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
while path!=file_path: while path!=file_path:
if len(file_path)>=len(path) and all([v==file_path[k] for k,v in enumerate(path)]): if len(file_path)>=len(path) and all([v==file_path[k] for k,v in enumerate(path)]):
path.append(file_path[len(path)]) path.append(file_path[len(path)])
thispath='static__'+'__'.join(path) thispath = regex_space.sub('-', 'static__'+'__'.join(path))
}} }}
<li class="folder"><i>&nbsp;</i> <li class="folder"><i>&nbsp;</i>
<a href="javascript:collapse('{{=thispath}}');" class="file">{{=path[-1]}}/</a> <a href="javascript:collapse('{{=thispath}}');" class="file">{{=path[-1]}}/</a>
@@ -25,6 +25,7 @@
<form action="{{=URL(args=request.args)}}" method="POST"> <form action="{{=URL(args=request.args)}}" method="POST">
<h2>{{=T('Select Files to Package')}}</h2> <h2>{{=T('Select Files to Package')}}</h2>
<input type="submit" value="{{=T('Download .w2p')}}" class="btn"/> <input type="submit" value="{{=T('Download .w2p')}}" class="btn"/>
<input type="submit" name="doexe" value="{{=T('Download as .exe')}}" class="btn"/>
<div style="margin-top:20px"> <div style="margin-top:20px">
{{tree(base)}} {{tree(base)}}
</div> </div>
+1 -1
View File
@@ -144,7 +144,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
{{=peekfile('views',c)}} {{=peekfile('views',c)}}
</span> </span>
<span class="extras celled"> <span class="extras celled">
{{if extend.has_key(c):}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}} {{if c in extend:}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
{{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}} {{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}}
</span> </span>
</li> </li>
+4 -1
View File
@@ -27,7 +27,9 @@
{{buttons.append((URL('pack',args=a), T("Pack all")))}} {{buttons.append((URL('pack',args=a), T("Pack all")))}}
{{buttons.append((URL('pack_custom',args=a), T("Pack custom")))}} {{buttons.append((URL('pack_custom',args=a), T("Pack custom")))}}
{{if not os.path.exists('applications/%s/compiled' % a):}} {{if not os.path.exists('applications/%s/compiled' % a):}}
{{buttons.append((URL('compile_app',args=a), T("Compile")))}} {{buttons.append((URL('compile_app',args=[a, 'skip_failed_views']),
T("Compile (skip failed views)")))}}
{{buttons.append((URL('compile_app',args=a), T("Compile (all or nothing)")))}}
{{else:}} {{else:}}
{{buttons.append((URL('pack',args=(a, 'compiled')), T("Pack compiled")))}} {{buttons.append((URL('pack',args=(a, 'compiled')), T("Pack compiled")))}}
{{if glob.glob('applications/%s/controllers/*.py' % a):}} {{if glob.glob('applications/%s/controllers/*.py' % a):}}
@@ -138,6 +140,7 @@
<p class="row-buttons"> <p class="row-buttons">
{{=button(URL('gae','deploy'), T('Deploy on Google App Engine'))}} {{=button(URL('gae','deploy'), T('Deploy on Google App Engine'))}}
{{=button(URL('openshift','deploy'),T('Deploy to OpenShift'))}} {{=button(URL('openshift','deploy'),T('Deploy to OpenShift'))}}
{{=button(URL('pythonanywhere','deploy'), T('Deploy to PythonAnywhere'))}}
</p> </p>
</div> <!-- /DEPLOY ON GAE --> </div> <!-- /DEPLOY ON GAE -->
<!-- APP WIZARD --> <!-- APP WIZARD -->
+1
View File
@@ -77,6 +77,7 @@
<script type="text/javascript"> <script type="text/javascript">
jQuery(document).ready(function(){ jQuery(document).ready(function(){
jQuery("[rel=tooltip]").tooltip(); jQuery("[rel=tooltip]").tooltip();
jQuery(":input").attr("autocomplete","off");
}); });
</script> </script>
<script> <script>
@@ -0,0 +1,176 @@
{{extend 'layout.html'}}
<h2><span style="color:#139FD7">python</span>anywhere {{=T('Deployment Interface')}}</h2>
<div id="register_form">
<h3>{{=T('Login/Register')}}</h3>
<form class="form-horizontal" id="palogin">
<div class="control-group" id="username__row">
<label class="control-label" for="username">{{=T('Username')}}</label>
<div class="controls">
<input type="text" name="username" id="username"><span class="help-inline">*</span>
<span class="help-block"></span>
</div>
</div>
<div class="control-group" id="email_address__row">
<label class="control-label" for="email_address">{{=T('Email Address')}}</label>
<div class="controls">
<input type="text" name="email_address" id="email_address">
<span class="help-block"></span>
</div>
</div>
<div class="control-group" id="pythonanywhere_password__row">
<label class="control-label" for="pythonanywhere_password">{{=T('PythonAnywhere Password')}}</label>
<div class="controls">
<input type="password" name="pythonanywhere_password" id="pythonanywhere_password">
<span class="help-block"></span>
</div>
</div>
<div class="control-group" id="web2py_admin_password__row">
<label class="control-label" for="web2py_admin_password">{{=T('web2py Admin Password')}}</label>
<div class="controls">
<input type="password" name="web2py_admin_password" id="web2py_admin_password"><span class="help-inline">*</span>
<span class="help-block"></span>
</div>
</div>
<div class="control-group" id="accepts_terms__row">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="accepts_terms" id="accepts_terms"><a target="_blank" href="https://www.pythonanywhere.com/terms/">{{=T('Accept Terms')}}</a>
</label>
<span class="help-block"></span>
</div>
</div>
<div class="control-group">
<div class="controls">
<button type="submit" class="btn btn-primary" id="submit_palogin">{{=T('Submit')}}</button>
</div>
</div>
</form>
<p>* {{=T('You only need these if you have already registered')}}</p>
</div>
<div class="row-fluid" id="app_manager" style="display:none;">
<div class="span6">
<h3>{{=T('Local Apps')}}</h3>
<form id="apppicker">
<select name="apps" class="form-control" id="local" multiple>
<option>{{=T('Loading...')}}</option>
</select>
<input type="submit" value="Deploy" id="deploy_button" class="btn btn-primary">
</form>
<div class="alert alert-info">
<strong>{{=T('Warning!')}}</strong> {{=T('if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.')}}
</div>
</div>
<div class="span6">
<h3>{{=T('PythonAnywhere Apps')}}</h3>
<ul id="pythonanywhere">
<li>{{=T('Loading...')}}</li>
</ul>
</div>
</div>
<script>
$(document).ready(function() {
$('#palogin').off('submit');
$('#palogin').submit(function(event) {
var data = $('#palogin').serialize();
$.web2py.disableElement($('#submit_palogin'));
$.web2py.disableFormElements($('#palogin'));
$.ajax({
url: '{{=URL("pythonanywhere", "create_account")}}',
type: 'POST',
data: data,
dataType: 'json',
}).done(function(data, textStatus, jqXHR) {
$('#palogin .error').removeClass('error');
$('#palogin .help-block').text('');
if(data.status == 'error') {
for(var error in data.errors) {
$('#' + error + '__row').addClass('error');
$('#' + error + '__row .help-block').text(data.errors[error][0]);
}
$.web2py.enableElement($('#submit_palogin'));
$.web2py.enableFormElements($('#palogin'));
$.web2py.flash("{{=T('Form has errors')}}");
} else {
$.web2py.flash("{{=T('Login successful')}}");
$('#register_form').hide();
$('#app_manager').show();
refresh_apps();
}
}).fail(function(){
$.web2py.flash("{{=T('Something went wrong please wait a few minutes before retrying')}}");
$.web2py.enableElement($('#submit_palogin'));
$.web2py.enableFormElements($('#palogin'));
});
event.preventDefault();
});
$('#apppicker').off('submit');
$('#apppicker').submit(function(event) {
var data = $('#apppicker').serialize();
$.web2py.disableElement($('#deploy_button'));
$.ajax({
url: '{{=URL("pythonanywhere", "bulk_install")}}',
type: 'POST',
data: {username: $('#username').val(), password: $('#web2py_admin_password').val(), apps: $('#local').val()},
dataType: 'json',
}).done(function(data, textStatus, jqXHR) {
refresh_apps();
$.web2py.enableElement($('#deploy_button'));
}).fail(function(){
$.web2py.flash("{{=T('Something went wrong please wait a few minutes before retrying')}}");
$.web2py.enableElement($('#deploy_button'));
});
event.preventDefault();
});
});
function refresh_apps() {
// Refresh List of Apps
$('#deploy_button').prop('disabled', true);
$.ajax({
url: '{{=URL("pythonanywhere", "list_apps")}}',
type: 'GET',
data: {username: $('#username').val(), password: $('#web2py_admin_password').val()},
dataType: 'json',
}).done(function(data, textStatus, jqXHR) {
var i = 0;
$('#local').html('')
for(i = 0; i < data.local.length; i++) {
$('#local').append($('<option>', {
value: data.local[i],
text: data.local[i]
}));
}
$('#local').multiSelect('refresh');
$('#pythonanywhere').html('')
for(i = 0; i < data.pythonanywhere.length; i++) {
$('#pythonanywhere').append($('<li>', {
text: data.pythonanywhere[i]
}));
}
$('#deploy_button').prop('disabled', false);
$.web2py.hide_flash();
}).fail(function(){
// Mostly this happens if it's a new account, just waiting a bit should be enough.
$.get('http://' + $('#username').val() + '.pythonanywhere.com'); // Kickstart the instance
$.web2py.flash("{{=T('Please wait, giving pythonanywhere a moment...')}}");
setTimeout(refresh_apps, 30000);
});
}
</script>
+30 -31
View File
@@ -49,7 +49,8 @@ if request.function == 'manage':
auth.table_group(), auth.table_group(),
auth.table_permission()]) auth.table_permission()])
manager_role = manager_action.get('role', None) if manager_action else None manager_role = manager_action.get('role', None) if manager_action else None
auth.requires_membership(manager_role)(lambda: None)() if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
raise HTTP(403, "Not authorized")
menu = False menu = False
elif (request.application == 'admin' and not session.authorized) or \ elif (request.application == 'admin' and not session.authorized) or \
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)): (request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
@@ -80,7 +81,6 @@ if False and request.tickets_db:
def get_databases(request): def get_databases(request):
dbs = {} dbs = {}
for (key, value) in global_env.items(): for (key, value) in global_env.items():
cond = False
try: try:
cond = isinstance(value, GQLDB) cond = isinstance(value, GQLDB)
except: except:
@@ -445,30 +445,31 @@ def ccache():
gae_stats['oldest'] = GetInHMS(time.time() - gae_stats['oldest_item_age']) gae_stats['oldest'] = GetInHMS(time.time() - gae_stats['oldest_item_age'])
total.update(gae_stats) total.update(gae_stats)
else: else:
# get ram stats directly from the cache object
ram_stats = cache.ram.stats[request.application]
ram['hits'] = ram_stats['hit_total'] - ram_stats['misses']
ram['misses'] = ram_stats['misses']
try:
ram['ratio'] = ram['hits'] * 100 / ram_stats['hit_total']
except (KeyError, ZeroDivisionError):
ram['ratio'] = 0
for key, value in cache.ram.storage.iteritems(): for key, value in cache.ram.storage.iteritems():
if isinstance(value, dict): if hp:
ram['hits'] = value['hit_total'] - value['misses'] ram['bytes'] += hp.iso(value[1]).size
ram['misses'] = value['misses'] ram['objects'] += hp.iso(value[1]).count
try: ram['entries'] += 1
ram['ratio'] = ram['hits'] * 100 / value['hit_total'] if value[0] < ram['oldest']:
except (KeyError, ZeroDivisionError): ram['oldest'] = value[0]
ram['ratio'] = 0 ram['keys'].append((key, GetInHMS(time.time() - value[0])))
else:
if hp:
ram['bytes'] += hp.iso(value[1]).size
ram['objects'] += hp.iso(value[1]).count
ram['entries'] += 1
if value[0] < ram['oldest']:
ram['oldest'] = value[0]
ram['keys'].append((key, GetInHMS(time.time() - value[0])))
for key in cache.disk.storage: for key in cache.disk.storage:
value = cache.disk.storage[key] value = cache.disk.storage[key]
if isinstance(value, dict): if isinstance(value[1], dict):
disk['hits'] = value['hit_total'] - value['misses'] disk['hits'] = value[1]['hit_total'] - value[1]['misses']
disk['misses'] = value['misses'] disk['misses'] = value[1]['misses']
try: try:
disk['ratio'] = disk['hits'] * 100 / value['hit_total'] disk['ratio'] = disk['hits'] * 100 / value[1]['hit_total']
except (KeyError, ZeroDivisionError): except (KeyError, ZeroDivisionError):
disk['ratio'] = 0 disk['ratio'] = 0
else: else:
@@ -480,12 +481,12 @@ def ccache():
disk['oldest'] = value[0] disk['oldest'] = value[0]
disk['keys'].append((key, GetInHMS(time.time() - value[0]))) disk['keys'].append((key, GetInHMS(time.time() - value[0])))
total['entries'] = ram['entries'] + disk['entries'] ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
total['bytes'] = ram['bytes'] + disk['bytes'] ram_keys.remove('ratio')
total['objects'] = ram['objects'] + disk['objects'] ram_keys.remove('oldest')
total['hits'] = ram['hits'] + disk['hits'] for key in ram_keys:
total['misses'] = ram['misses'] + disk['misses'] total[key] = ram[key] + disk[key]
total['keys'] = ram['keys'] + disk['keys']
try: try:
total['ratio'] = total['hits'] * 100 / (total['hits'] + total['ratio'] = total['hits'] * 100 / (total['hits'] +
total['misses']) total['misses'])
@@ -575,11 +576,9 @@ def bg_graph_model():
meta_graphmodel = dict(group=request.application, color='#ECECEC') meta_graphmodel = dict(group=request.application, color='#ECECEC')
group = meta_graphmodel['group'].replace(' ', '') group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group): if group not in subgraphs:
subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename) subgraphs[group]['tables'].append(tablename)
else:
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext', graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename)) label=table_template(tablename))
@@ -8,6 +8,8 @@
#### Learning and Demos #### Learning and Demos
- [[Intro video http://www.youtube.com/watch?v=BXzqmHx6edY]] and [[code examples https://github.com/mjhea0/web2py]] - [[Intro video http://www.youtube.com/watch?v=BXzqmHx6edY]] and [[code examples https://github.com/mjhea0/web2py]]
- [[Step by step tutorial https://milesm.pythonanywhere.com/wiki]]
- [[web2py Reference Project http://www.web2pyref.com/]]
- [[Killer Web Development Tutorial http://killer-web-development.com/]] - [[Killer Web Development Tutorial http://killer-web-development.com/]]
- [[Real Python for the Web http://www.realpython.com]] (web development with web2py and more!) - [[Real Python for the Web http://www.realpython.com]] (web development with web2py and more!)
- [[Admin Demo http://www.web2py.com/demo_admin popup]] (web-based IDE) - [[Admin Demo http://www.web2py.com/demo_admin popup]] (web-based IDE)
@@ -22,5 +24,6 @@
- [[More Plugins http://dev.s-cubism.com/web2py_plugins]] - [[More Plugins http://dev.s-cubism.com/web2py_plugins]]
- [[Appliances http://www.web2py.com/appliances popup]] - [[Appliances http://www.web2py.com/appliances popup]]
- [[web2py utils http://packages.python.org/web2py_utils/ popup]] - [[web2py utils http://packages.python.org/web2py_utils/ popup]]
- [[Sublime text 3 plugin https://bitbucket.org/kfog/w2p popup]]
#### [[Sites Powered by web2py http://www.web2py.com/poweredby popup]] #### [[Sites Powered by web2py http://www.web2py.com/poweredby popup]]
@@ -320,3 +320,10 @@ li.w2p_grid_breadcrumb_elem {
.ie-lte8 div.flash{ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#222222', endColorstr='#000000', GradientType=0 ); } .ie-lte8 div.flash{ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#222222', endColorstr='#000000', GradientType=0 ); }
.ie-lte8 div.flash:hover {filter:alpha(opacity=25);} .ie-lte8 div.flash:hover {filter:alpha(opacity=25);}
.ie9 #w2p_query_panel {padding-bottom:2px} .ie9 #w2p_query_panel {padding-bottom:2px}
.control-label.readonly{
padding-top:0px !important;
padding-right:0px !important;
}
File diff suppressed because one or more lines are too long
+39 -29
View File
@@ -75,8 +75,8 @@
* this over and over... all will be bound to the document * this over and over... all will be bound to the document
*/ */
/*adds btn class to buttons*/ /*adds btn class to buttons*/
$('button', target).addClass('btn'); $('button:not([class^="btn"])', target).addClass('btn');
$('form input[type="submit"], form input[type="button"]', target).addClass('btn'); $('form input[type="submit"]:not([class^="btn"]), form input[type="button"]:not([class^="btn"])', target).addClass('btn');
/* javascript for PasswordWidget*/ /* javascript for PasswordWidget*/
$('input[type=password][data-w2p_entropy]', target).each(function() { $('input[type=password][data-w2p_entropy]', target).each(function() {
web2py.validate_entropy($(this)); web2py.validate_entropy($(this));
@@ -133,7 +133,7 @@
}, },
ajax_init: function(target) { ajax_init: function(target) {
/*called whenever a fragment gets loaded */ /*called whenever a fragment gets loaded */
$('.hidden', target).hide(); $('.w2p_hidden', target).hide();
web2py.manage_errors(target); web2py.manage_errors(target);
web2py.ajax_fields(target); web2py.ajax_fields(target);
web2py.show_if_handler(target); web2py.show_if_handler(target);
@@ -161,7 +161,7 @@
* and require no dom manipulations * and require no dom manipulations
*/ */
var doc = $(document); var doc = $(document);
doc.on('click', '.flash', function(e) { doc.on('click', '.w2p_flash', function(e) {
var t = $(this); var t = $(this);
if(t.css('top') == '0px') t.slideUp('slow'); if(t.css('top') == '0px') t.slideUp('slow');
else t.fadeOut(); else t.fadeOut();
@@ -241,7 +241,7 @@
/*personally I don't like it. /*personally I don't like it.
*if there's an error it it flashed and can be removed *if there's an error it it flashed and can be removed
*as any other message *as any other message
*doc.off('click', '.flash') *doc.off('click', '.w2p_flash')
*/ */
switch(xhr.status) { switch(xhr.status) {
case 500: case 500:
@@ -257,12 +257,20 @@
if(form.hasClass('no_trap')) { if(form.hasClass('no_trap')) {
return; return;
} }
form.attr('data-w2p_target', target);
var w2p_target = $(this).attr('data-w2p_target');
if (typeof w2p_target === typeof undefined || w2p_target === false) {
form.attr('data-w2p_target', target);
} else {
target = w2p_target;
}
var url = form.attr('action'); var url = form.attr('action');
if((url === "") || (url === "#") || (typeof url === 'undefined')) { if((url === "") || (url === "#") || (typeof url === 'undefined')) {
/* form has no action. Use component url. */ /* form has no action. Use component url. */
url = action; url = action;
} }
form.submit(function(e) { form.submit(function(e) {
web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.disableElement(form.find(web2py.formInputClickSelector));
web2py.hide_flash(); web2py.hide_flash();
@@ -490,12 +498,12 @@
* and prevent clicking on it */ * and prevent clicking on it */
disableElement: function(el) { disableElement: function(el) {
el.addClass('disabled'); el.addClass('disabled');
var method = el.is('button') ? 'html' : 'val'; var method = el.is('input') ? 'val' : 'html';
//method = el.attr('name') ? 'html' : 'val'; //method = el.attr('name') ? 'html' : 'val';
var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state if not already disabled */ /*store enabled state if not already disabled */
if(el.data('w2p:enable-with') === undefined) { if(el.data('w2p_enable_with') === undefined) {
el.data('w2p:enable-with', el[method]()); el.data('w2p_enable_with', el[method]());
} }
/*if you don't want to see "working..." on buttons, replace the following /*if you don't want to see "working..." on buttons, replace the following
* two lines with this one * two lines with this one
@@ -515,11 +523,11 @@
/* restore element to its original state which was disabled by 'disableElement' above*/ /* restore element to its original state which was disabled by 'disableElement' above*/
enableElement: function(el) { enableElement: function(el) {
var method = el.is('button') ? 'val' : 'html'; var method = el.is('input') ? 'val' : 'html';
if(el.data('w2p:enable-with') !== undefined) { if(el.data('w2p_enable_with') !== undefined) {
/* set to old enabled state */ /* set to old enabled state */
el[method](el.data('w2p:enable-with')); el[method](el.data('w2p_enable_with'));
el.removeData('w2p:enable-with'); el.removeData('w2p_enable_with');
} }
el.removeClass('disabled'); el.removeClass('disabled');
el.unbind('click.w2pDisable'); el.unbind('click.w2pDisable');
@@ -530,13 +538,13 @@
}, },
/*helper for flash messages*/ /*helper for flash messages*/
flash: function(message, status) { flash: function(message, status) {
var flash = $('.flash'); var flash = $('.w2p_flash');
web2py.hide_flash(); web2py.hide_flash();
flash.html(message).addClass(status); flash.html(message).addClass(status);
if(flash.html()) flash.append('<span id="closeflash"> &times; </span>').slideDown(); if(flash.html()) flash.append('<span id="closeflash"> &times; </span>').slideDown();
}, },
hide_flash: function() { hide_flash: function() {
$('.flash').fadeOut(0).html(''); $('.w2p_flash').fadeOut(0).html('');
}, },
show_if_handler: function(target) { show_if_handler: function(target) {
var triggers = {}; var triggers = {};
@@ -586,12 +594,14 @@
if(pre_call != undefined) { if(pre_call != undefined) {
eval(pre_call); eval(pre_call);
} }
if(confirm_message != undefined) { if(confirm_message) {
if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; if(confirm_message == 'default')
if(!web2py.confirm(confirm_message)) { confirm_message = w2p_ajax_confirm_message ||
web2py.stopEverything(e); 'Are you sure you want to delete this object?';
return; if(!web2py.confirm(confirm_message)) {
} web2py.stopEverything(e);
return;
}
} }
if(target == undefined) { if(target == undefined) {
if(method == 'GET') { if(method == 'GET') {
@@ -634,7 +644,7 @@
}); });
}, },
/* Disables form elements: /* Disables form elements:
- Caches element value in 'w2p:enable-with' data store - Caches element value in 'w2p_enable_with' data store
- Replaces element text with value of 'data-disable-with' attribute - Replaces element text with value of 'data-disable-with' attribute
- Sets disabled property to true - Sets disabled property to true
*/ */
@@ -646,8 +656,8 @@
if(disable_with == undefined) { if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]()) element.data('w2p_disable_with', element[method]())
} }
if(element.data('w2p:enable-with') === undefined) { if(element.data('w2p_enable_with') === undefined) {
element.data('w2p:enable-with', element[method]()); element.data('w2p_enable_with', element[method]());
} }
element[method](element.data('w2p_disable_with')); element[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
@@ -655,16 +665,16 @@
}, },
/* Re-enables disabled form elements: /* Re-enables disabled form elements:
- Replaces element text with cached value from 'w2p:enable-with' data store (created in `disableFormElements`) - Replaces element text with cached value from 'w2p_enable_with' data store (created in `disableFormElements`)
- Sets disabled property to false - Sets disabled property to false
*/ */
enableFormElements: function(form) { enableFormElements: function(form) {
form.find(web2py.enableSelector).each(function() { form.find(web2py.enableSelector).each(function() {
var element = $(this), var element = $(this),
method = element.is('button') ? 'html' : 'val'; method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) { if(element.data('w2p_enable_with')) {
element[method](element.data('w2p:enable-with')); element[method](element.data('w2p_enable_with'));
element.removeData('w2p:enable-with'); element.removeData('w2p_enable_with');
} }
element.prop('disabled', false); element.prop('disabled', false);
}); });
@@ -690,7 +700,7 @@
$.web2py.component_handler(target); $.web2py.component_handler(target);
}, },
main_hook: function() { main_hook: function() {
var flash = $('.flash'); var flash = $('.w2p_flash');
flash.hide(); flash.hide();
if(flash.html()) web2py.flash(flash.html()); if(flash.html()) web2py.flash(flash.html());
web2py.ajax_init(document); web2py.ajax_init(document);
+9 -9
View File
@@ -137,9 +137,9 @@
<h4>{{=T("Overview")}}</h4> <h4>{{=T("Overview")}}</h4>
<p>{{=T.M("Number of entries: **%s**", total['entries'])}}</p> <p>{{=T.M("Number of entries: **%s**", total['entries'])}}</p>
{{if total['entries'] > 0:}} {{if total['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=total['ratio'], hits=total['hits'], misses=total['misses']))}} dict( ratio=total['ratio'], hits=total['hits'], misses=total['misses']))}}
</p> </p>
<p> <p>
{{=T("Size of cache:")}} {{=T("Size of cache:")}}
{{if object_stats:}} {{if object_stats:}}
@@ -155,8 +155,8 @@
{{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.", {{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}} dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}}
</p> </p>
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle();')}} {{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "w2p_hidden" );')}}
<div class="hidden" id="all_keys"> <div class="w2p_hidden" id="all_keys">
{{=total['keys']}} {{=total['keys']}}
</div> </div>
<br /> <br />
@@ -183,8 +183,8 @@
{{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.", {{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}} dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}}
</p> </p>
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle();')}} {{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "w2p_hidden" );')}}
<div class="hidden" id="ram_keys"> <div class="w2p_hidden" id="ram_keys">
{{=ram['keys']}} {{=ram['keys']}}
</div> </div>
<br /> <br />
@@ -212,8 +212,8 @@
{{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.", {{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}} dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}}
</p> </p>
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle();')}} {{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "w2p_hidden" );')}}
<div class="hidden" id="disk_keys"> <div class="w2p_hidden" id="disk_keys">
{{=disk['keys']}} {{=disk['keys']}}
</div> </div>
<br /> <br />
@@ -36,7 +36,7 @@
</center> </center>
<p style="text-align:left;"> <p style="text-align:left;">
The source code version works on all supported platforms, including Linux, but it requires Python 2.5, 2.6, or 2.7. The source code version works on all supported platforms, including Linux, but it requires Python 2.6, or 2.7 (recommended).
It runs on Windows and most Unix systems, including <b>Linux</b> and <b>BSD</b>. It runs on Windows and most Unix systems, including <b>Linux</b> and <b>BSD</b>.
</p> </p>
@@ -17,19 +17,24 @@
<ul> <ul>
<li><a target="_blank" href="http://experts4solutions.com">Experts4Soutions</a> (worldwide)</li> <li><a target="_blank" href="http://experts4solutions.com">Experts4Soutions</a> (worldwide)</li>
<li><a target="_blank" href="http://www.planethost.com">PlanetHost</a> (USA)</li> <li><a target="_blank" href="http://www.planethost.com">PlanetHost</a> (USA)</li>
<li><a target="_blank" href="http://www.10biosystems.com">10BioSystems</a> (USA)</li>
<li><a target="_blank" href="http://www.formatics.nl">Formatics</a> (Netherlands)</li> <li><a target="_blank" href="http://www.formatics.nl">Formatics</a> (Netherlands)</li>
<li><a target="_blank" href="http://www.corebyte.nl">Corebyte</a> (Netherlands)</li>
<li><a target="_blank" href="http://www.dutveul.nl">Dutveul</a> (Netherlands)</li>
<li><a target="_blank" href="http://www.onemewebservices.com">OneMeWebServices</a> (Canada)</li> <li><a target="_blank" href="http://www.onemewebservices.com">OneMeWebServices</a> (Canada)</li>
<li><a target="_blank" href="http://www.budgetbytes.nl">BudgetBytes</a> (The Netherlands)</li> <li><a target="_blank" href="http://www.budgetbytes.nl">BudgetBytes</a> (The Netherlands)</li>
<li><a target="_blank" href="http://www.androsoft.pl">ANDROSoft</a> (Poland)</li> <li><a target="_blank" href="http://www.androsoft.pl">ANDROSoft</a> (Poland)</li>
<li><a target="_blank" href="www.sonnetech.com.br">Sonne Tech</a> (Brazil)</li> <li><a target="_blank" href="http://www.sonnetech.com.br">Sonne Tech</a> (Brazil)</li>
<li><a target="_blank" href="http://www.nrg.com.br">NRG Internet Solutions</a> (Brazil)</li> <li><a target="_blank" href="http://www.nrg.com.br">NRG Internet Solutions</a> (Brazil)</li>
<li><a target="_blank" href="http://itjp.net.br/">ITJP</a> (Brazil)</li> <li><a target="_blank" href="http://itjp.net.br/">ITJP</a> (Brazil)</li>
<li><a target="_blank" href="http://i-am.pt">I am Consultoria</a> (Portugal)</li> <li><a target="_blank" href="http://i-am.pt">I am Consultoria</a> (Portugal)</li>
<li><a target="_blank" href="http://www.definescope.com/">DefineScope</a> (Portugal)</li>
<li><a target="_blank" href="http://lpfx.com.br">LPFX</a> (Brazil)</li> <li><a target="_blank" href="http://lpfx.com.br">LPFX</a> (Brazil)</li>
<li><a target="_blank" href="http://emotionull.com">Emotionull</a> (Greece and Cyprus)</li> <li><a target="_blank" href="http://emotionull.com">Emotionull</a> (Greece and Cyprus)</li>
<li><a target="_blank" href="http://www.vsa-services.com/">VSA Services</a> (Singapore)</li> <li><a target="_blank" href="http://www.vsa-services.com/">VSA Services</a> (Singapore)</li>
<li><a target="_blank" href="http://www.albendas.com">Albendas</a> (Spain)</li> <li><a target="_blank" href="http://www.albendas.com">Albendas</a> (Spain)</li>
<li><a target="_blank" href="www.corebyte.nl">Corebyte</a> (Netherland)</li>
<li><a target="_blank" href="https://loadinfo-net.appspot.com">LoadInfo</a> (Bulgaria)</li> <li><a target="_blank" href="https://loadinfo-net.appspot.com">LoadInfo</a> (Bulgaria)</li>
<li><a target="_blank" href="http://www.appliedobjects.com">Applied Objects</a> (New Zealand)</li> <li><a target="_blank" href="http://www.appliedobjects.com">Applied Objects</a> (New Zealand)</li>
<li><a target="_blank" href="http://www.sistemasagiles.com.ar/">Sistemas Ágiles</a> ("Agile Systems") (Argentina)</li> <li><a target="_blank" href="http://www.sistemasagiles.com.ar/">Sistemas Ágiles</a> ("Agile Systems") (Argentina)</li>
+30 -31
View File
@@ -49,7 +49,8 @@ if request.function == 'manage':
auth.table_group(), auth.table_group(),
auth.table_permission()]) auth.table_permission()])
manager_role = manager_action.get('role', None) if manager_action else None manager_role = manager_action.get('role', None) if manager_action else None
auth.requires_membership(manager_role)(lambda: None)() if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
raise HTTP(403, "Not authorized")
menu = False menu = False
elif (request.application == 'admin' and not session.authorized) or \ elif (request.application == 'admin' and not session.authorized) or \
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)): (request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
@@ -80,7 +81,6 @@ if False and request.tickets_db:
def get_databases(request): def get_databases(request):
dbs = {} dbs = {}
for (key, value) in global_env.items(): for (key, value) in global_env.items():
cond = False
try: try:
cond = isinstance(value, GQLDB) cond = isinstance(value, GQLDB)
except: except:
@@ -445,30 +445,31 @@ def ccache():
gae_stats['oldest'] = GetInHMS(time.time() - gae_stats['oldest_item_age']) gae_stats['oldest'] = GetInHMS(time.time() - gae_stats['oldest_item_age'])
total.update(gae_stats) total.update(gae_stats)
else: else:
# get ram stats directly from the cache object
ram_stats = cache.ram.stats[request.application]
ram['hits'] = ram_stats['hit_total'] - ram_stats['misses']
ram['misses'] = ram_stats['misses']
try:
ram['ratio'] = ram['hits'] * 100 / ram_stats['hit_total']
except (KeyError, ZeroDivisionError):
ram['ratio'] = 0
for key, value in cache.ram.storage.iteritems(): for key, value in cache.ram.storage.iteritems():
if isinstance(value, dict): if hp:
ram['hits'] = value['hit_total'] - value['misses'] ram['bytes'] += hp.iso(value[1]).size
ram['misses'] = value['misses'] ram['objects'] += hp.iso(value[1]).count
try: ram['entries'] += 1
ram['ratio'] = ram['hits'] * 100 / value['hit_total'] if value[0] < ram['oldest']:
except (KeyError, ZeroDivisionError): ram['oldest'] = value[0]
ram['ratio'] = 0 ram['keys'].append((key, GetInHMS(time.time() - value[0])))
else:
if hp:
ram['bytes'] += hp.iso(value[1]).size
ram['objects'] += hp.iso(value[1]).count
ram['entries'] += 1
if value[0] < ram['oldest']:
ram['oldest'] = value[0]
ram['keys'].append((key, GetInHMS(time.time() - value[0])))
for key in cache.disk.storage: for key in cache.disk.storage:
value = cache.disk.storage[key] value = cache.disk.storage[key]
if isinstance(value, dict): if isinstance(value[1], dict):
disk['hits'] = value['hit_total'] - value['misses'] disk['hits'] = value[1]['hit_total'] - value[1]['misses']
disk['misses'] = value['misses'] disk['misses'] = value[1]['misses']
try: try:
disk['ratio'] = disk['hits'] * 100 / value['hit_total'] disk['ratio'] = disk['hits'] * 100 / value[1]['hit_total']
except (KeyError, ZeroDivisionError): except (KeyError, ZeroDivisionError):
disk['ratio'] = 0 disk['ratio'] = 0
else: else:
@@ -480,12 +481,12 @@ def ccache():
disk['oldest'] = value[0] disk['oldest'] = value[0]
disk['keys'].append((key, GetInHMS(time.time() - value[0]))) disk['keys'].append((key, GetInHMS(time.time() - value[0])))
total['entries'] = ram['entries'] + disk['entries'] ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
total['bytes'] = ram['bytes'] + disk['bytes'] ram_keys.remove('ratio')
total['objects'] = ram['objects'] + disk['objects'] ram_keys.remove('oldest')
total['hits'] = ram['hits'] + disk['hits'] for key in ram_keys:
total['misses'] = ram['misses'] + disk['misses'] total[key] = ram[key] + disk[key]
total['keys'] = ram['keys'] + disk['keys']
try: try:
total['ratio'] = total['hits'] * 100 / (total['hits'] + total['ratio'] = total['hits'] * 100 / (total['hits'] +
total['misses']) total['misses'])
@@ -575,11 +576,9 @@ def bg_graph_model():
meta_graphmodel = dict(group=request.application, color='#ECECEC') meta_graphmodel = dict(group=request.application, color='#ECECEC')
group = meta_graphmodel['group'].replace(' ', '') group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group): if group not in subgraphs:
subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename) subgraphs[group]['tables'].append(tablename)
else:
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext', graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename)) label=table_template(tablename))
+2 -1
View File
@@ -29,11 +29,12 @@ def user():
http://..../[app]/default/user/profile http://..../[app]/default/user/profile
http://..../[app]/default/user/retrieve_password http://..../[app]/default/user/retrieve_password
http://..../[app]/default/user/change_password http://..../[app]/default/user/change_password
http://..../[app]/default/user/manage_users (requires membership in http://..../[app]/default/user/bulk_register
use @auth.requires_login() use @auth.requires_login()
@auth.requires_membership('group name') @auth.requires_membership('group name')
@auth.requires_permission('read','table name',record_id) @auth.requires_permission('read','table name',record_id)
to decorate functions that need access control to decorate functions that need access control
also notice there is http://..../[app]/appadmin/manage/auth to allow administrator to manage users
""" """
return dict(form=auth()) return dict(form=auth())
+1 -1
View File
@@ -62,7 +62,7 @@ auth.define_tables(username=False, signature=False)
## configure email ## configure email
mail = auth.settings.mailer mail = auth.settings.mailer
mail.settings.server = 'logging' if request.is_local else myconf.take('smtp.sender') mail.settings.server = 'logging' if request.is_local else myconf.take('smtp.server')
mail.settings.sender = myconf.take('smtp.sender') mail.settings.sender = myconf.take('smtp.sender')
mail.settings.login = myconf.take('smtp.login') mail.settings.login = myconf.take('smtp.login')
+2 -2
View File
@@ -7,11 +7,11 @@
# Language from default.py or 'en' (if the file is not found) is used as # Language from default.py or 'en' (if the file is not found) is used as
# a default_language # a default_language
# #
# See <web2py-root-dir>/router.example.py for parameter's detail # See <web2py-root-dir>/examples/routes.parametric.example.py for parameter's detail
#------------------------------------------------------------------------------------- #-------------------------------------------------------------------------------------
# To enable this route file you must do the steps: # To enable this route file you must do the steps:
# #
# 1. rename <web2py-root-dir>/router.example.py to routes.py # 1. rename <web2py-root-dir>/examples/routes.parametric.example.py to routes.py
# 2. rename this APP/routes.example.py to APP/routes.py # 2. rename this APP/routes.example.py to APP/routes.py
# (where APP - is your application directory) # (where APP - is your application directory)
# 3. restart web2py (or reload routes in web2py admin interfase) # 3. restart web2py (or reload routes in web2py admin interfase)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
div.flash { div.w2p_flash {
background-image: none; background-image: none;
border-radius: 4px; border-radius: 4px;
-o-border-radius: 4px; -o-border-radius: 4px;
@@ -15,16 +15,18 @@ div.flash {
margin: 0 0 20px; margin: 0 0 20px;
padding: 15px 35px 15px 15px; padding: 15px 35px 15px 15px;
} }
div.flash.alert:hover { div.w2p_flash.alert:hover {
opacity: 1; opacity: 1;
} }
.ie-lte8 div.flash { .ie-lte8 div.w2p_flash {
filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#222222', endColorstr='#000000', GradientType=0); filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#222222', endColorstr='#000000', GradientType=0);
} }
.ie-lte8 div.flash:hover { .ie-lte8 div.w2p_flash:hover {
filter: alpha(opacity=25); filter: alpha(opacity=25);
} }
.main-container {
margin-top: 20px;
}
div.error { div.error {
width: auto; width: auto;
@@ -35,7 +37,7 @@ div.error {
display: inline-block; display: inline-block;
padding: 5px; padding: 5px;
} }
div.flash.alert { div.w2p_flash.alert {
display: none; display: none;
position: fixed; position: fixed;
top: 70px; top: 70px;
@@ -134,7 +136,7 @@ header h1 {
header .jumbotron { header .jumbotron {
background-color: transparent; background-color: transparent;
} }
.flash { .w2p_flash {
opacity: 0.9!important; opacity: 0.9!important;
right: 100px; right: 100px;
} }
@@ -283,6 +285,7 @@ li.w2p_grid_breadcrumb_elem {
.web2py_console .form-control { .web2py_console .form-control {
width: 20%; width: 20%;
display: inline; display: inline;
height: 100%;
} }
.web2py_console #w2p_keywords { .web2py_console #w2p_keywords {
width: 50%; width: 50%;
@@ -311,6 +314,3 @@ td.w2p_fc,
input[type=checkbox], input[type=radio] { input[type=checkbox], input[type=radio] {
margin: 4px 4px 0 0; margin: 4px 4px 0 0;
} }
.btn {
margin-right: 4px;
}
+6 -6
View File
@@ -33,7 +33,7 @@ audio {width:200px}
[type="text"], [type="password"], select { [type="text"], [type="password"], select {
margin-right: 5px; width: 300px; margin-right: 5px; width: 300px;
} }
.hidden {display:none;visibility:visible} .w2p_hidden {display:none;visibility:visible}
.right {float:right; text-align:right} .right {float:right; text-align:right}
.left {float:left; text-align:left} .left {float:left; text-align:left}
.center {width:100%; text-align:center; vertical-align:middle} .center {width:100%; text-align:center; vertical-align:middle}
@@ -88,7 +88,7 @@ div.w2p_export_menu a, div.w2p_wiki_tags a, div.w2p_cloud a {margin-left:5px; pa
#web2py_user_form td {vertical-align:top} #web2py_user_form td {vertical-align:top}
/*********** web2py specific ***********/ /*********** web2py specific ***********/
div.flash { div.w2p_flash {
font-weight:bold; font-weight:bold;
display:none; display:none;
position:fixed; position:fixed;
@@ -117,11 +117,11 @@ div.flash {
z-index:2000; z-index:2000;
} }
div.flash #closeflash{color:inherit; float:right; margin-left:15px;} div.w2p_flash #closeflash{color:inherit; float:right; margin-left:15px;}
.ie-lte7 div.flash #closeflash .ie-lte7 div.flash #closeflash
{color:expression(this.parentNode.currentStyle['color']);float:none;position:absolute;right:4px;} {color:expression(this.parentNode.currentStyle['color']);float:none;position:absolute;right:4px;}
div.flash:hover { opacity:0.25; } div.w2p_flash:hover { opacity:0.25; }
div.error_wrapper {display:block} div.error_wrapper {display:block}
div.error { div.error {
@@ -304,8 +304,8 @@ li.w2p_grid_breadcrumb_elem {
/* fix some IE problems */ /* fix some IE problems */
.ie-lte7 .topbar .container {z-index:2} .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.w2p_flash{ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#222222', endColorstr='#000000', GradientType=0 ); }
.ie-lte8 div.flash:hover {filter:alpha(opacity=25);} .ie-lte8 div.w2p_flash:hover {filter:alpha(opacity=25);}
.ie9 #w2p_query_panel {padding-bottom:2px} .ie9 #w2p_query_panel {padding-bottom:2px}
.web2py_console .form-control {width: 20%; display: inline;} .web2py_console .form-control {width: 20%; display: inline;}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,9 +6,9 @@
* this over and over... all will be bound to the document * this over and over... all will be bound to the document
*/ */
/*adds btn class to buttons*/ /*adds btn class to buttons*/
$('button', target).addClass('btn btn-default'); $('button:not([class^="btn"])', target).addClass('btn btn-default');
$("p.w2p-autocomplete-widget input").addClass('form-control'); $("p.w2p-autocomplete-widget input").addClass('form-control');
$('form input[type="submit"], form input[type="button"]', target).addClass('btn btn-default'); $('form input[type="submit"]:not([class^="btn"]), form input[type="button"]:not([class^="btn"])', target).addClass('btn btn-default');
/* javascript for PasswordWidget*/ /* javascript for PasswordWidget*/
$('input[type=password][data-w2p_entropy]', target).each(function() { $('input[type=password][data-w2p_entropy]', target).each(function() {
web2py.validate_entropy($(this)); web2py.validate_entropy($(this));
@@ -18,9 +18,9 @@
function pe(ul, e) { function pe(ul, e) {
var new_line = ml(ul); var new_line = ml(ul);
rel(ul); rel(ul);
if ($(e.target).parent().is(':visible')) { if ($(e.target).closest('li').is(':visible')) {
/* make sure we didn't delete the element before we insert after */ /* make sure we didn't delete the element before we insert after */
new_line.insertAfter($(e.target).parent()); new_line.insertAfter($(e.target).closest('li'));
} else { } else {
/* the line we clicked on was deleted, just add to end of list */ /* the line we clicked on was deleted, just add to end of list */
new_line.appendTo(ul); new_line.appendTo(ul);
@@ -30,9 +30,9 @@
} }
function rl(ul, e) { function rl(ul, e) {
if ($(ul).children().length > 1) { if ($(ul).find('li').length > 1) {
/* only remove if we have more than 1 item so the list is never empty */ /* only remove if we have more than 1 item so the list is never empty */
$(e.target).parent().remove(); $(e.target).closest('li').remove();
} }
} }
@@ -46,13 +46,13 @@
function rel(ul) { function rel(ul) {
/* keep only as many as needed*/ /* keep only as many as needed*/
$(ul).find("li").each(function() { $(ul).find("li").each(function() {
var trimmed = $.trim($(this.firstChild).val()); var trimmed = $.trim($(this).find(":text").val());
if (trimmed == '') $(this).remove(); if (trimmed == '') $(this).remove();
else $(this.firstChild).val(trimmed); else $(this).find(":text").val(trimmed);
}); });
} }
var ul = this; var ul = this;
$(ul).find(":text").after('<a class="btn btn-default" href="#">+</a>&nbsp;<a class="btn btn-default" href="#">-</a>').keypress(function(e) { $(ul).find(":text").addClass('form-control').wrap("<div class='input-group'></div>").after('<div class="input-group-addon"><i class="glyphicon glyphicon-plus"></i></div><div class="input-group-addon"><i class="glyphicon glyphicon-minus"></i></div>').keypress(function(e) {
return (e.which == 13) ? pe(ul, e) : true; return (e.which == 13) ? pe(ul, e) : true;
}).next().click(function(e) { }).next().click(function(e) {
pe(ul, e); pe(ul, e);
+25 -15
View File
@@ -75,8 +75,8 @@
* this over and over... all will be bound to the document * this over and over... all will be bound to the document
*/ */
/*adds btn class to buttons*/ /*adds btn class to buttons*/
$('button', target).addClass('btn'); $('button:not([class^="btn"])', target).addClass('btn');
$('form input[type="submit"], form input[type="button"]', target).addClass('btn'); $('form input[type="submit"]:not([class^="btn"]), form input[type="button"]:not([class^="btn"])', target).addClass('btn');
/* javascript for PasswordWidget*/ /* javascript for PasswordWidget*/
$('input[type=password][data-w2p_entropy]', target).each(function() { $('input[type=password][data-w2p_entropy]', target).each(function() {
web2py.validate_entropy($(this)); web2py.validate_entropy($(this));
@@ -133,7 +133,7 @@
}, },
ajax_init: function(target) { ajax_init: function(target) {
/*called whenever a fragment gets loaded */ /*called whenever a fragment gets loaded */
$('.hidden', target).hide(); $('.w2p_hidden', target).hide();
web2py.manage_errors(target); web2py.manage_errors(target);
web2py.ajax_fields(target); web2py.ajax_fields(target);
web2py.show_if_handler(target); web2py.show_if_handler(target);
@@ -161,7 +161,7 @@
* and require no dom manipulations * and require no dom manipulations
*/ */
var doc = $(document); var doc = $(document);
doc.on('click', '.flash', function(e) { doc.on('click', '.w2p_flash', function(e) {
var t = $(this); var t = $(this);
if(t.css('top') == '0px') t.slideUp('slow'); if(t.css('top') == '0px') t.slideUp('slow');
else t.fadeOut(); else t.fadeOut();
@@ -241,7 +241,7 @@
/*personally I don't like it. /*personally I don't like it.
*if there's an error it it flashed and can be removed *if there's an error it it flashed and can be removed
*as any other message *as any other message
*doc.off('click', '.flash') *doc.off('click', '.w2p_flash')
*/ */
switch(xhr.status) { switch(xhr.status) {
case 500: case 500:
@@ -257,12 +257,20 @@
if(form.hasClass('no_trap')) { if(form.hasClass('no_trap')) {
return; return;
} }
form.attr('data-w2p_target', target);
var w2p_target = $(this).attr('data-w2p_target');
if (typeof w2p_target === typeof undefined || w2p_target === false) {
form.attr('data-w2p_target', target);
} else {
target = w2p_target;
}
var url = form.attr('action'); var url = form.attr('action');
if((url === "") || (url === "#") || (typeof url === 'undefined')) { if((url === "") || (url === "#") || (typeof url === 'undefined')) {
/* form has no action. Use component url. */ /* form has no action. Use component url. */
url = action; url = action;
} }
form.submit(function(e) { form.submit(function(e) {
web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.disableElement(form.find(web2py.formInputClickSelector));
web2py.hide_flash(); web2py.hide_flash();
@@ -530,13 +538,13 @@
}, },
/*helper for flash messages*/ /*helper for flash messages*/
flash: function(message, status) { flash: function(message, status) {
var flash = $('.flash'); var flash = $('.w2p_flash');
web2py.hide_flash(); web2py.hide_flash();
flash.html(message).addClass(status); flash.html(message).addClass(status);
if(flash.html()) flash.append('<span id="closeflash"> &times; </span>').slideDown(); if(flash.html()) flash.append('<span id="closeflash"> &times; </span>').slideDown();
}, },
hide_flash: function() { hide_flash: function() {
$('.flash').fadeOut(0).html(''); $('.w2p_flash').fadeOut(0).html('');
}, },
show_if_handler: function(target) { show_if_handler: function(target) {
var triggers = {}; var triggers = {};
@@ -586,12 +594,14 @@
if(pre_call != undefined) { if(pre_call != undefined) {
eval(pre_call); eval(pre_call);
} }
if(confirm_message != undefined) { if(confirm_message) {
if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; if(confirm_message == 'default')
if(!web2py.confirm(confirm_message)) { confirm_message = w2p_ajax_confirm_message ||
web2py.stopEverything(e); 'Are you sure you want to delete this object?';
return; if(!web2py.confirm(confirm_message)) {
} web2py.stopEverything(e);
return;
}
} }
if(target == undefined) { if(target == undefined) {
if(method == 'GET') { if(method == 'GET') {
@@ -690,7 +700,7 @@
$.web2py.component_handler(target); $.web2py.component_handler(target);
}, },
main_hook: function() { main_hook: function() {
var flash = $('.flash'); var flash = $('.w2p_flash');
flash.hide(); flash.hide();
if(flash.html()) web2py.flash(flash.html()); if(flash.html()) web2py.flash(flash.html());
web2py.ajax_init(document); web2py.ajax_init(document);
+9 -9
View File
@@ -137,9 +137,9 @@
<h4>{{=T("Overview")}}</h4> <h4>{{=T("Overview")}}</h4>
<p>{{=T.M("Number of entries: **%s**", total['entries'])}}</p> <p>{{=T.M("Number of entries: **%s**", total['entries'])}}</p>
{{if total['entries'] > 0:}} {{if total['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=total['ratio'], hits=total['hits'], misses=total['misses']))}} dict( ratio=total['ratio'], hits=total['hits'], misses=total['misses']))}}
</p> </p>
<p> <p>
{{=T("Size of cache:")}} {{=T("Size of cache:")}}
{{if object_stats:}} {{if object_stats:}}
@@ -155,8 +155,8 @@
{{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.", {{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}} dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}}
</p> </p>
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle();')}} {{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "w2p_hidden" );')}}
<div class="hidden" id="all_keys"> <div class="w2p_hidden" id="all_keys">
{{=total['keys']}} {{=total['keys']}}
</div> </div>
<br /> <br />
@@ -183,8 +183,8 @@
{{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.", {{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}} dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}}
</p> </p>
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle();')}} {{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "w2p_hidden" );')}}
<div class="hidden" id="ram_keys"> <div class="w2p_hidden" id="ram_keys">
{{=ram['keys']}} {{=ram['keys']}}
</div> </div>
<br /> <br />
@@ -212,8 +212,8 @@
{{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.", {{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}} dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}}
</p> </p>
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle();')}} {{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "w2p_hidden" );')}}
<div class="hidden" id="disk_keys"> <div class="w2p_hidden" id="disk_keys">
{{=disk['keys']}} {{=disk['keys']}}
</div> </div>
<br /> <br />
+3 -4
View File
@@ -21,7 +21,6 @@
<meta name="google-site-verification" content=""> <meta name="google-site-verification" content="">
<!-- include stylesheets --> <!-- include stylesheets -->
<link rel="stylesheet" href="{{=URL('static','css/bootstrap.min.css')}}"/> <link rel="stylesheet" href="{{=URL('static','css/bootstrap.min.css')}}"/>
<link rel="stylesheet" href="{{=URL('static','css/bootstrap-theme.min.css')}}"/>
<link rel="stylesheet" href="{{=URL('static','css/web2py-bootstrap3.css')}}"/> <link rel="stylesheet" href="{{=URL('static','css/web2py-bootstrap3.css')}}"/>
<link rel="shortcut icon" href="{{=URL('static','images/favicon.ico')}}" type="image/x-icon"> <link rel="shortcut icon" href="{{=URL('static','images/favicon.ico')}}" type="image/x-icon">
<link rel="apple-touch-icon" href="{{=URL('static','images/favicon.png')}}"> <link rel="apple-touch-icon" href="{{=URL('static','images/favicon.png')}}">
@@ -47,9 +46,9 @@
</head> </head>
<body> <body>
<!--[if lt IE 8]><p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p><![endif]--> <!--[if lt IE 8]><p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p><![endif]-->
<div class="flash alert alert-dismissable">{{=response.flash or ''}}</div> <div class="w2p_flash alert alert-dismissable">{{=response.flash or ''}}</div>
<!-- Navbar ======================================= --> <!-- Navbar ======================================= -->
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-header"> <div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
@@ -75,7 +74,7 @@
{{end}} {{end}}
<!-- Main ========================================= --> <!-- Main ========================================= -->
<!-- Begin page content --> <!-- Begin page content -->
<div class="container-fluid"> <div class="container-fluid main-container">
{{if left_sidebar_enabled:}} {{if left_sidebar_enabled:}}
<div class="col-md-3 left-sidebar"> <div class="col-md-3 left-sidebar">
{{block left_sidebar}} {{block left_sidebar}}
+25
View File
@@ -0,0 +1,25 @@
build: false
environment:
matrix:
- PYTHON: "C:/Python27"
COVERAGE_PROCESS_START: gluon/tests/coverage.ini
clone_depth: 50
init:
- "ECHO %PYTHON%"
- set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%
install:
- ps: Start-FileDownload https://bootstrap.pypa.io/get-pip.py
- python get-pip.py
- pip install codecov
- git submodule update --init --recursive
test_script:
- python web2py.py --run_system_tests --with_coverage
after_test:
- coverage combine
- codecov
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- app configuration for web2py on IIS -->
<configuration>
<appSettings>
<add key="WSGI_HANDLER" value="gluon.main.wsgibase" />
<add key="WSGI_RESTART_FILE_REGEX" value=".*((routes\.py)|(\.config))$" />
</appSettings>
<system.webServer>
<rewrite>
<rules>
<clear />
<rule name="static" enabled="true" stopProcessing="true">
<match url="^(\w+)/static(?:/_[\d]+\.[\d]+\.[\d]+)?/(.*)$" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
<action type="Rewrite" url="applications/{R:1}/static/{R:2}" logRewrittenUrl="false" />
</rule>
<rule name="web2py_app" enabled="true" stopProcessing="true">
<match url="(.*)" ignoreCase="false" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
<action type="Rewrite" url="handler.web2py/{R:1}" appendQueryString="true" />
</rule>
</rules>
<outboundRules>
<rule name="static_version_cache_control" preCondition="static_version">
<match serverVariable="RESPONSE_Cache-Control" pattern=".*" />
<action type="Rewrite" value="max-age=315360000" />
<conditions>
</conditions>
</rule>
<rule name="static_version_Expires" preCondition="static_version">
<match serverVariable="RESPONSE_Expires" pattern=".*" />
<action type="Rewrite" value="Thu, 31 Dec 2037 23:59:59 GMT" />
</rule>
<preConditions>
<preCondition name="static_version">
<add input="{REQUEST_URI}" pattern="(\w+)/static(?:/_[\d]+\.[\d]+\.[\d]+)?/(.*)$" />
</preCondition>
</preConditions>
</outboundRules>
</rewrite>
<handlers>
<!-- replace SCRIPT_PROCESSOR with the configured handler for python -->
<add name="Python_via_FastCGI" path="handler.web2py" verb="*" modules="FastCgiModule" scriptProcessor="SCRIPT_PROCESSOR" resourceType="Unspecified" requireAccess="Script" />
</handlers>
</system.webServer>
</configuration>
Vendored
+150
View File
@@ -0,0 +1,150 @@
from fabric.api import *
from fabric.operations import put, get
from fabric.contrib.files import exists
import os
import datetime
import getpass
env.hosts = env.hosts or raw_input('hostname (example.com):').split(',')
env.user = env.user or raw_input('username :')
INSTALL_SCRIPT = "setup-web2py-nginx-uwsgi-ubuntu.sh"
now = datetime.datetime.now()
applications = '/home/www-data/web2py/applications'
def create_user(username):
"""fab -H root@host create_user:username"""
password = getpass.getpass(name+' password for %s> ' % username)
run('useradd -m %s' % username)
run('usermod --password %s %s' % (crypt.crypt(password, 'salt'), username))
run('mkdir -p ~%s/.ssh' % username)
run('cp /etc/sudoers /tmp/sudoers.new')
append('/tmp/sudoers.new', '%s ALL=NOPASSWD: ALL' % username, use_sudo=True)
run('visudo -c -f /tmp/sudoers.new')
run('EDITOR="cp /tmp/sudoers.new" visudo')
uncomment('~%s/.bashrc' % username, '#force_color_prompt=yes')
local('ssh-copy-id %s' % env.hosts[0])
def install_web2py():
"""fab -H username@host install_web2py"""
sudo('wget https://raw.githubusercontent.com/web2py/web2py/master/scripts/%s' % INSTALL_SCRIPT)
sudo('chmod +x %s' % INSTALL_SCRIPT)
sudo('./'+INSTALL_SCRIPT)
def start_webserver():
sudo('service nginx start')
sudo('start uwsgi-emperor')
sudo('start web2py-scheduler')
def stop_webserver():
sudo('stop uwsgi-emperor')
sudo('service nginx stop')
sudo('stop web2py-scheduler')
def restart_webserver():
stop_webserver()
start_webserver()
def notify(appname=None):
"""fab -H username@host notify:appname"""
appname = appname or os.path.split(os.getcwd())[-1]
appfolder = applications+'/'+appname
with cd(appfolder):
sudo('echo "response.flash = \'System Going Down For Maintenance\'" > models/flash_goingdown.py')
def down(appname=None):
"""fab -H username@host down:appname"""
appname = appname or os.path.split(os.getcwd())[-1]
appfolder = applications+'/'+appname
with cd(appfolder):
sudo('echo `date` > DISABLED')
sudo('rm -rf sessions/* || true')
def up(appname=None):
"""fab -H username@host up:appname"""
appname = appname or os.path.split(os.getcwd())[-1]
appfolder = applications+'/'+appname
with cd(appfolder):
if exists('modules/flash_goingdown.py'):
sudo('rm modules/flash_goingdown.py')
sudo('rm DISABLED')
def mkdir_or_backup(appname):
appfolder = applications+'/'+appname
if not exists(appfolder):
sudo('mkdir %s' % appfolder)
sudo('chown -R www-data:www-data %s' % appfolder)
backup = None
else:
dt = now.strftime('%y-%m-%d-%h-%m')
backup = '%s.%s.zip' % (appname, dt)
with cd(applications):
sudo('zip -r %s %s' % (backup, appname))
return backup
def git_deploy(appname, repo):
"""fab -H username@host git_deploy:appname,username/remoname"""
appfolder = applications+'/'+appname
backup = mkdir_or_backup(appfolder)
if exists(appfolder):
with cd(appfolder):
sudo('git pull origin master')
sudo('chown -R www-data:www-data *')
else:
with cd(applications):
sudo('git clone git@github.com/%s %s' % (repo, name))
sudo('chown -R www-data:www-data %s' % name)
def retrieve(appname=None):
"""fab -H username@host retrieve:appname"""
appname = appname or os.path.split(os.getcwd())[-1]
appfolder = applications+'/'+appname
filename = '%s.zip' % appname
with cd(appfolder):
sudo('zip -r /tmp/%s *' % filename)
get('/tmp/%s' % filename, filename)
sudo('rm /tmp/%s' % filename)
local('unzip %s' % filename)
local('rm %s' % filename)
def deploy(appname=None, all=False):
"""fab -H username@host deploy:appname,all"""
appname = appname or os.path.split(os.getcwd())[-1]
appfolder = applications+'/'+appname
if os.path.exists('_update.zip'):
os.unlink('_update.zip')
backup = mkdir_or_backup(appfolder)
if all=='all' or not backup:
local('zip -r _update.zip * -x *~ -x .* -x \#* -x *.bak -x *.bak2')
else:
local('zip -r _update.zip */*.py views/*.html views/*/*.html static/*')
put('_update.zip','/tmp/_update.zip')
with cd(appfolder):
sudo('unzip -o /tmp/_update.zip')
sudo('chown -R www-data:www-data *')
sudo('echo "%s" > DATE_DEPLOYMENT' % now)
if backup:
print 'TO RESTORE: fab restore:%s' % backup
def restore(backup):
"""fab -H username@host restore:backupfilename"""
appname = backup.split('/')[-1].split('.')[0]
appfolder = applications + '/' + appname
with cd(appfolder):
sudo('rm -r *')
with cd(applications):
sudo('unzip %s' % backup)
sudo('chown -R www-data:www-data %s' % appname)
def cleanup(appname):
appname = appname or os.path.split(os.getcwd())[-1]
appfolder = applications + '/' + appname
with cd(appfolder):
sudo('rm -rf sessions/* || true')
sudo('rm -rf errors/* || true')
sudo('rm -rf cache/* || true')
+6 -5
View File
@@ -59,6 +59,8 @@ def app_pack(app, request, raise_ex=False, filenames=None):
w2p_pack(filename, apath(app, request), filenames=filenames) w2p_pack(filename, apath(app, request), filenames=filenames)
return filename return filename
except Exception, e: except Exception, e:
import traceback
print traceback.format_exc()
if raise_ex: if raise_ex:
raise raise
return False return False
@@ -119,9 +121,8 @@ def app_cleanup(app, request):
# Remove cache files # Remove cache files
path = apath('%s/cache/' % app, request) path = apath('%s/cache/' % app, request)
CacheOnDisk(folder=path).clear()
if os.path.exists(path): if os.path.exists(path):
CacheOnDisk(folder=path).clear()
for f in os.listdir(path): for f in os.listdir(path):
try: try:
if f[:1] != '.': recursive_unlink(os.path.join(path, f)) if f[:1] != '.': recursive_unlink(os.path.join(path, f))
@@ -130,7 +131,7 @@ def app_cleanup(app, request):
return r return r
def app_compile(app, request): def app_compile(app, request, skip_failed_views=False):
"""Compiles the application """Compiles the application
Args: Args:
@@ -144,8 +145,8 @@ def app_compile(app, request):
from compileapp import compile_application, remove_compiled_application from compileapp import compile_application, remove_compiled_application
folder = apath(app, request) folder = apath(app, request)
try: try:
compile_application(folder) failed_views = compile_application(folder, skip_failed_views)
return None return failed_views
except (Exception, RestrictedError): except (Exception, RestrictedError):
tb = traceback.format_exc(sys.exc_info) tb = traceback.format_exc(sys.exc_info)
remove_compiled_application(folder) remove_compiled_application(folder)
+18 -9
View File
@@ -473,7 +473,12 @@ class CacheOnDisk(CacheAbstract):
if item and ((dt is None) or (item[0] > now - dt)): if item and ((dt is None) or (item[0] > now - dt)):
value = item[1] value = item[1]
else: else:
value = f() try:
value = f()
except:
self.storage.release(CacheAbstract.cache_stats_name)
self.storage.release(key)
raise
self.storage[key] = (now, value) self.storage[key] = (now, value)
self.storage.safe_apply(CacheAbstract.cache_stats_name, inc_misses, self.storage.safe_apply(CacheAbstract.cache_stats_name, inc_misses,
default_value={'hit_total': 0, 'misses': 0}) default_value={'hit_total': 0, 'misses': 0})
@@ -601,22 +606,26 @@ class Cache(object):
def wrapped_f(): def wrapped_f():
if current.request.env.request_method != 'GET': if current.request.env.request_method != 'GET':
return func() return func()
if quick:
session_ = True if 'S' in quick else False
vars_ = True if 'V' in quick else False
lang_ = True if 'L' in quick else False
user_agent_ = True if 'U' in quick else False
public_ = True if 'P' in quick else False
else:
(session_, vars_, lang_, user_agent_, public_) = \
(session, vars, lang, user_agent, public)
if time_expire: if time_expire:
cache_control = 'max-age=%(time_expire)s, s-maxage=%(time_expire)s' % dict(time_expire=time_expire) cache_control = 'max-age=%(time_expire)s, s-maxage=%(time_expire)s' % dict(time_expire=time_expire)
if quick:
session_ = True if 'S' in quick else False
vars_ = True if 'V' in quick else False
lang_ = True if 'L' in quick else False
user_agent_ = True if 'U' in quick else False
public_ = True if 'P' in quick else False
else:
session_, vars_, lang_, user_agent_, public_ = session, vars, lang, user_agent, public
if not session_ and public_: if not session_ and public_:
cache_control += ', public' cache_control += ', public'
expires = (current.request.utcnow + datetime.timedelta(seconds=time_expire)).strftime('%a, %d %b %Y %H:%M:%S GMT') expires = (current.request.utcnow + datetime.timedelta(seconds=time_expire)).strftime('%a, %d %b %Y %H:%M:%S GMT')
else: else:
cache_control += ', private' cache_control += ', private'
expires = 'Fri, 01 Jan 1990 00:00:00 GMT' expires = 'Fri, 01 Jan 1990 00:00:00 GMT'
if cache_model: if cache_model:
#figure out the correct cache key #figure out the correct cache key
cache_key = [current.request.env.path_info, current.response.view] cache_key = [current.request.env.path_info, current.response.view]
+46 -42
View File
@@ -261,7 +261,7 @@ class LoadFactory(object):
import globals import globals
target = target or 'c' + str(random.random())[2:] target = target or 'c' + str(random.random())[2:]
attr['_id'] = target attr['_id'] = target
request = self.environment['request'] request = current.request
if '.' in f: if '.' in f:
f, extension = f.rsplit('.', 1) f, extension = f.rsplit('.', 1)
if url or ajax: if url or ajax:
@@ -464,22 +464,28 @@ def read_pyc(filename):
return marshal.loads(data[8:]) return marshal.loads(data[8:])
def compile_views(folder): def compile_views(folder, skip_failed_views=False):
""" """
Compiles all the views in the application specified by `folder` Compiles all the views in the application specified by `folder`
""" """
path = pjoin(folder, 'views') path = pjoin(folder, 'views')
failed_views = []
for fname in listdir(path, '^[\w/\-]+(\.\w+)*$'): for fname in listdir(path, '^[\w/\-]+(\.\w+)*$'):
try: try:
data = parse_template(fname, path) data = parse_template(fname, path)
except Exception, e: except Exception, e:
raise Exception("%s in %s" % (e, fname)) if skip_failed_views:
filename = 'views.%s.py' % fname.replace(os.path.sep, '.') failed_views.append(fname)
filename = pjoin(folder, 'compiled', filename) else:
write_file(filename, data) raise Exception("%s in %s" % (e, fname))
save_pyc(filename) else:
os.unlink(filename) filename = ('views/%s.py' % fname).replace('/', '_').replace('\\', '_')
filename = pjoin(folder, 'compiled', filename)
write_file(filename, data)
save_pyc(filename)
os.unlink(filename)
return failed_views if failed_views else None
def compile_models(folder): def compile_models(folder):
@@ -532,10 +538,11 @@ def run_models_in(environment):
It tries pre-compiled models first before compiling them. It tries pre-compiled models first before compiling them.
""" """
folder = environment['request'].folder request = current.request
c = environment['request'].controller folder = request.folder
c = request.controller
#f = environment['request'].function #f = environment['request'].function
response = environment['response'] response = current.response
path = pjoin(folder, 'models') path = pjoin(folder, 'models')
cpath = pjoin(folder, 'compiled') cpath = pjoin(folder, 'compiled')
@@ -577,7 +584,7 @@ def run_controller_in(controller, function, environment):
""" """
# if compiled should run compiled! # if compiled should run compiled!
folder = environment['request'].folder folder = current.request.folder
path = pjoin(folder, 'compiled') path = pjoin(folder, 'compiled')
badc = 'invalid controller (%s/%s)' % (controller, function) badc = 'invalid controller (%s/%s)' % (controller, function)
badf = 'invalid function (%s/%s)' % (controller, function) badf = 'invalid function (%s/%s)' % (controller, function)
@@ -631,7 +638,7 @@ def run_controller_in(controller, function, environment):
layer = filename + ':' + function layer = filename + ':' + function
code = getcfs(layer, filename, lambda: compile2(code, layer)) code = getcfs(layer, filename, lambda: compile2(code, layer))
restricted(code, environment, filename) restricted(code, environment, filename)
response = environment['response'] response = current.response
vars = response._vars vars = response._vars
if response.postprocessing: if response.postprocessing:
vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars) vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars)
@@ -649,9 +656,9 @@ 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 = current.request
response = environment['response'] response = current.response
view = response.view view = environment['response'].view
folder = request.folder folder = request.folder
path = pjoin(folder, 'compiled') path = pjoin(folder, 'compiled')
badv = 'invalid view (%s)' % view badv = 'invalid view (%s)' % view
@@ -666,32 +673,28 @@ def run_view_in(environment):
ccode = parse_template(view, pjoin(folder, 'views'), ccode = parse_template(view, pjoin(folder, 'views'),
context=environment) context=environment)
restricted(ccode, environment, 'file stream') restricted(ccode, environment, 'file stream')
elif os.path.exists(path):
x = view.replace('/', '.')
files = ['views.%s.pyc' % x]
if allow_generic:
files.append('views.generic.%s.pyc' % request.extension)
# for backward compatibility
x = view.replace('/', '_')
files.append('views_%s.pyc' % x)
if allow_generic:
files.append('views_generic.%s.pyc' % request.extension)
if request.extension == 'html':
files.append('views_%s.pyc' % x[:-5])
if allow_generic:
files.append('views_generic.pyc')
# end backward compatibility code
for f in files:
filename = pjoin(path, f)
if os.path.exists(filename):
code = read_pyc(filename)
restricted(code, environment, layer=filename)
return
raise HTTP(404,
rewrite.THREAD_LOCAL.routes.error_message % badv,
web2py_error=badv)
else: else:
filename = pjoin(folder, 'views', view) filename = pjoin(folder, 'views', view)
if os.path.exists(path): # compiled views
x = view.replace('/', '_')
files = ['views_%s.pyc' % x]
is_compiled = os.path.exists(pjoin(path, files[0]))
# Don't use a generic view if the non-compiled view exists.
if is_compiled or (not is_compiled and not os.path.exists(filename)):
if allow_generic:
files.append('views_generic.%s.pyc' % request.extension)
# for backward compatibility
if request.extension == 'html':
files.append('views_%s.pyc' % x[:-5])
if allow_generic:
files.append('views_generic.pyc')
# end backward compatibility code
for f in files:
compiled = pjoin(path, f)
if os.path.exists(compiled):
code = read_pyc(compiled)
restricted(code, environment, layer=compiled)
return
if not os.path.exists(filename) and allow_generic: if not os.path.exists(filename) and allow_generic:
view = 'generic.' + request.extension view = 'generic.' + request.extension
filename = pjoin(folder, 'views', view) filename = pjoin(folder, 'views', view)
@@ -725,7 +728,7 @@ def remove_compiled_application(folder):
pass pass
def compile_application(folder): def compile_application(folder, skip_failed_views=False):
""" """
Compiles all models, views, controller for the application in `folder`. Compiles all models, views, controller for the application in `folder`.
""" """
@@ -733,7 +736,8 @@ def compile_application(folder):
os.mkdir(pjoin(folder, 'compiled')) os.mkdir(pjoin(folder, 'compiled'))
compile_models(folder) compile_models(folder)
compile_controllers(folder) compile_controllers(folder)
compile_views(folder) failed_views = compile_views(folder, skip_failed_views)
return failed_views
def test(): def test():
+1 -1
View File
@@ -93,7 +93,7 @@ def video(url):
def googledoc_viewer(url): def googledoc_viewer(url):
return '<iframe src="http://docs.google.com/viewer?url=%s&embedded=true" style="max-width:100%%"></iframe>' % urllib.quote(url) return '<iframe src="https://docs.google.com/viewer?url=%s&embedded=true" style="max-width:100%%"></iframe>' % urllib.quote(url)
def web2py_component(url): def web2py_component(url):
+12 -7
View File
@@ -6,15 +6,17 @@
# #
# Given the model # Given the model
# #
# db.define_table("table_name", Field("picture", "upload"), Field("thumbnail", "upload")) # db.define_table("table_name", Field("picture", "upload"),
# Field("thumbnail", "upload"))
# #
# # to resize the picture on upload # to resize the picture on upload
# #
# from images import RESIZE # from images import RESIZE
# #
# db.table_name.picture.requires = RESIZE(200, 200) # db.table_name.picture.requires = RESIZE(200, 200)
# #
# # to store original image in picture and create a thumbnail in 'thumbnail' field # to store original image in picture and create a thumbnail
# in 'thumbnail' field
# #
# from images import THUMB # from images import THUMB
# db.table_name.thumbnail.compute = lambda row: THUMB(row.picture, 200, 200) # db.table_name.thumbnail.compute = lambda row: THUMB(row.picture, 200, 200)
@@ -24,8 +26,11 @@ from gluon import current
class RESIZE(object): class RESIZE(object):
def __init__(self, nx=160, ny=80, error_message=' image resize'):
(self.nx, self.ny, self.error_message) = (nx, ny, error_message) def __init__(self, nx=160, ny=80, quality=100,
error_message=' image resize'):
(self.nx, self.ny, self.quality, self.error_message) = (
nx, ny, quality, error_message)
def __call__(self, value): def __call__(self, value):
if isinstance(value, str) and len(value) == 0: if isinstance(value, str) and len(value) == 0:
@@ -36,7 +41,7 @@ class RESIZE(object):
img = Image.open(value.file) img = Image.open(value.file)
img.thumbnail((self.nx, self.ny), Image.ANTIALIAS) img.thumbnail((self.nx, self.ny), Image.ANTIALIAS)
s = cStringIO.StringIO() s = cStringIO.StringIO()
img.save(s, 'JPEG', quality=100) img.save(s, 'JPEG', quality=self.quality)
s.seek(0) s.seek(0)
value.file = s value.file = s
except: except:
@@ -51,7 +56,7 @@ def THUMB(image, nx=120, ny=120, gae=False, name='thumb'):
request = current.request request = current.request
from PIL import Image from PIL import Image
import os import os
img = Image.open(os.path.join(request.folder,'uploads',image)) img = Image.open(os.path.join(request.folder, 'uploads', image))
img.thumbnail((nx, ny), Image.ANTIALIAS) img.thumbnail((nx, ny), Image.ANTIALIAS)
root, ext = os.path.splitext(image) root, ext = os.path.splitext(image)
thumb = '%s_%s%s' % (root, name, ext) thumb = '%s_%s%s' % (root, name, ext)
+101 -103
View File
@@ -14,12 +14,20 @@ except Exception, e:
raise e raise e
def ldap_auth(server='ldap', port=None, def ldap_auth(server='ldap',
port=None,
base_dn='ou=users,dc=domain,dc=com', base_dn='ou=users,dc=domain,dc=com',
mode='uid', secure=False, mode='uid',
cert_path=None, cert_file=None, secure=False,
cacert_path=None, cacert_file=None, key_file=None, self_signed_certificate=None, # See NOTE below
bind_dn=None, bind_pw=None, filterstr='objectClass=*', cert_path=None,
cert_file=None,
cacert_path=None,
cacert_file=None,
key_file=None,
bind_dn=None,
bind_pw=None,
filterstr='objectClass=*',
username_attrib='uid', username_attrib='uid',
custom_scope='subtree', custom_scope='subtree',
allowed_groups=None, allowed_groups=None,
@@ -33,6 +41,7 @@ def ldap_auth(server='ldap', port=None,
group_name_attrib='cn', group_name_attrib='cn',
group_member_attrib='memberUid', group_member_attrib='memberUid',
group_filterstr='objectClass=*', group_filterstr='objectClass=*',
tls=False,
logging_level='error'): logging_level='error'):
""" """
@@ -80,6 +89,13 @@ def ldap_auth(server='ldap', port=None,
If ldap is using GnuTLS then you need cert_file="..." instead cert_path If ldap is using GnuTLS then you need cert_file="..." instead cert_path
because cert_path isn't implemented in GnuTLS :( because cert_path isn't implemented in GnuTLS :(
To enable TLS, set tls=True:
auth.settings.login_methods.append(ldap_auth(
server='my.ldap.server',
base_dn='ou=Users,dc=domain,dc=com',
tls=True))
If you need to bind to the directory with an admin account in order to If you need to bind to the directory with an admin account in order to
search it then specify bind_dn & bind_pw to use for this. search it then specify bind_dn & bind_pw to use for this.
- currently only implemented for Active Directory - currently only implemented for Active Directory
@@ -151,6 +167,14 @@ def ldap_auth(server='ldap', port=None,
You can set the logging level with the "logging_level" parameter, default You can set the logging level with the "logging_level" parameter, default
is "error" and can be set to error, warning, info, debug. is "error" and can be set to error, warning, info, debug.
""" """
if self_signed_certificate:
# NOTE : If you have a self-signed SSL Certificate pointing over "port=686" and "secure=True" alone
# will not work, you need also to set "self_signed_certificate=True".
# Ref1: https://onemoretech.wordpress.com/2015/06/25/connecting-to-ldap-over-self-signed-tls-with-python/
# Ref2: http://bneijt.nl/blog/post/connecting-to-ldaps-with-self-signed-cert-using-python/
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
logger = logging.getLogger('web2py.auth.ldap_auth') logger = logging.getLogger('web2py.auth.ldap_auth')
if logging_level == 'error': if logging_level == 'error':
logger.setLevel(logging.ERROR) logger.setLevel(logging.ERROR)
@@ -188,8 +212,7 @@ def ldap_auth(server='ldap', port=None,
logger.warning('blank password not allowed') logger.warning('blank password not allowed')
return False return False
logger.debug('mode: [%s] manage_user: [%s] custom_scope: [%s]' logger.debug('mode: [%s] manage_user: [%s] custom_scope: [%s]'
' manage_groups: [%s]' % (str(mode), str(manage_user), ' manage_groups: [%s]' % (str(mode), str(manage_user), str(custom_scope), str(manage_groups)))
str(custom_scope), str(manage_groups)))
if manage_user: if manage_user:
if user_firstname_attrib.count(':') > 0: if user_firstname_attrib.count(':') > 0:
(user_firstname_attrib, (user_firstname_attrib,
@@ -238,14 +261,10 @@ def ldap_auth(server='ldap', port=None,
# in the ldap_basedn # in the ldap_basedn
requested_attrs = ['sAMAccountName'] requested_attrs = ['sAMAccountName']
if manage_user: if manage_user:
requested_attrs.extend([user_firstname_attrib, requested_attrs.extend([user_firstname_attrib, user_lastname_attrib, user_mail_attrib])
user_lastname_attrib,
user_mail_attrib])
result = con.search_ext_s( result = con.search_ext_s(
ldap_basedn, ldap.SCOPE_SUBTREE, ldap_basedn, ldap.SCOPE_SUBTREE,
"(&(sAMAccountName=%s)(%s))" % ( "(&(sAMAccountName=%s)(%s))" % (ldap.filter.escape_filter_chars(username_bare), filterstr),
ldap.filter.escape_filter_chars(username_bare),
filterstr),
requested_attrs)[0][1] requested_attrs)[0][1]
if not isinstance(result, dict): if not isinstance(result, dict):
# result should be a dict in the form # result should be a dict in the form
@@ -278,25 +297,21 @@ def ldap_auth(server='ldap', port=None,
if manage_user: if manage_user:
result = con.search_s(dn, ldap.SCOPE_BASE, result = con.search_s(dn, ldap.SCOPE_BASE,
"(objectClass=*)", "(objectClass=*)",
[user_firstname_attrib, [user_firstname_attrib, user_lastname_attrib, user_mail_attrib])[0][1]
user_lastname_attrib,
user_mail_attrib])[0][1]
if ldap_mode == 'uid': if ldap_mode == 'uid':
# OpenLDAP (UID) # OpenLDAP (UID)
if ldap_binddn and ldap_bindpw: if ldap_binddn and ldap_bindpw:
con.simple_bind_s(ldap_binddn, ldap_bindpw) con.simple_bind_s(ldap_binddn, ldap_bindpw)
dn = "uid=" + username + "," + ldap_basedn dn = "uid=" + username + "," + ldap_basedn
dn = con.search_s(ldap_basedn, ldap.SCOPE_SUBTREE, "(uid=%s)"%username, [''])[0][0] dn = con.search_s(ldap_basedn, ldap.SCOPE_SUBTREE, "(uid=%s)" % username, [''])[0][0]
else: else:
dn = "uid=" + username + "," + ldap_basedn dn = "uid=" + username + "," + ldap_basedn
con.simple_bind_s(dn, password) con.simple_bind_s(dn, password)
if manage_user: if manage_user:
result = con.search_s(dn, ldap.SCOPE_BASE, result = con.search_s(dn, ldap.SCOPE_BASE,
"(objectClass=*)", "(objectClass=*)",
[user_firstname_attrib, [user_firstname_attrib, user_lastname_attrib, user_mail_attrib])[0][1]
user_lastname_attrib,
user_mail_attrib])[0][1]
if ldap_mode == 'company': if ldap_mode == 'company':
# no DNs or password needed to search directory # no DNs or password needed to search directory
@@ -311,9 +326,7 @@ def ldap_auth(server='ldap', port=None,
# find the uid # find the uid
attrs = ['uid'] attrs = ['uid']
if manage_user: if manage_user:
attrs.extend([user_firstname_attrib, attrs.extend([user_firstname_attrib, user_lastname_attrib, user_mail_attrib])
user_lastname_attrib,
user_mail_attrib])
# perform the actual search # perform the actual search
company_search_result = con.search_s(ldap_basedn, company_search_result = con.search_s(ldap_basedn,
ldap.SCOPE_SUBTREE, ldap.SCOPE_SUBTREE,
@@ -329,13 +342,11 @@ def ldap_auth(server='ldap', port=None,
basedns = ldap_basedn basedns = ldap_basedn
else: else:
basedns = [ldap_basedn] basedns = [ldap_basedn]
filter = '(&(uid=%s)(%s))' % ( filter = '(&(uid=%s)(%s))' % (ldap.filter.escape_filter_chars(username), filterstr)
ldap.filter.escape_filter_chars(username), filterstr)
found = False found = False
for basedn in basedns: for basedn in basedns:
try: try:
result = con.search_s(basedn, ldap.SCOPE_SUBTREE, result = con.search_s(basedn, ldap.SCOPE_SUBTREE, filter)
filter)
if result: if result:
user_dn = result[0][0] user_dn = result[0][0]
# Check the password # Check the password
@@ -344,9 +355,10 @@ def ldap_auth(server='ldap', port=None,
break break
except ldap.LDAPError, detail: except ldap.LDAPError, detail:
(exc_type, exc_value) = sys.exc_info()[:2] (exc_type, exc_value) = sys.exc_info()[:2]
logger.warning( logger.warning("ldap_auth: searching %s for %s resulted in %s: %s\n" % (basedn,
"ldap_auth: searching %s for %s resulted in %s: %s\n" % filter,
(basedn, filter, exc_type, exc_value) exc_type,
exc_value)
) )
if not found: if not found:
logger.warning('User [%s] not found!' % username) logger.warning('User [%s] not found!' % username)
@@ -359,10 +371,7 @@ def ldap_auth(server='ldap', port=None,
basedns = ldap_basedn basedns = ldap_basedn
else: else:
basedns = [ldap_basedn] basedns = [ldap_basedn]
filter = '(&(%s=%s)(%s))' % (username_attrib, filter = '(&(%s=%s)(%s))' % (username_attrib, ldap.filter.escape_filter_chars(username), filterstr)
ldap.filter.escape_filter_chars(
username),
filterstr)
if custom_scope == 'subtree': if custom_scope == 'subtree':
ldap_scope = ldap.SCOPE_SUBTREE ldap_scope = ldap.SCOPE_SUBTREE
elif custom_scope == 'base': elif custom_scope == 'base':
@@ -381,9 +390,10 @@ def ldap_auth(server='ldap', port=None,
break break
except ldap.LDAPError, detail: except ldap.LDAPError, detail:
(exc_type, exc_value) = sys.exc_info()[:2] (exc_type, exc_value) = sys.exc_info()[:2]
logger.warning( logger.warning("ldap_auth: searching %s for %s resulted in %s: %s\n" % (basedn,
"ldap_auth: searching %s for %s resulted in %s: %s\n" % filter,
(basedn, filter, exc_type, exc_value) exc_type,
exc_value)
) )
if not found: if not found:
logger.warning('User [%s] not found!' % username) logger.warning('User [%s] not found!' % username)
@@ -393,16 +403,14 @@ def ldap_auth(server='ldap', port=None,
logger.info('[%s] Manage user data' % str(username)) logger.info('[%s] Manage user data' % str(username))
try: try:
if user_firstname_part is not None: if user_firstname_part is not None:
store_user_firstname = result[user_firstname_attrib][ store_user_firstname = result[user_firstname_attrib][0].split(' ', 1)[user_firstname_part]
0].split(' ', 1)[user_firstname_part]
else: else:
store_user_firstname = result[user_firstname_attrib][0] store_user_firstname = result[user_firstname_attrib][0]
except KeyError, e: except KeyError, e:
store_user_firstname = None store_user_firstname = None
try: try:
if user_lastname_part is not None: if user_lastname_part is not None:
store_user_lastname = result[user_lastname_attrib][ store_user_lastname = result[user_lastname_attrib][0].split(' ', 1)[user_lastname_part]
0].split(' ', 1)[user_lastname_part]
else: else:
store_user_lastname = result[user_lastname_attrib][0] store_user_lastname = result[user_lastname_attrib][0]
except KeyError, e: except KeyError, e:
@@ -411,32 +419,27 @@ def ldap_auth(server='ldap', port=None,
store_user_mail = result[user_mail_attrib][0] store_user_mail = result[user_mail_attrib][0]
except KeyError, e: except KeyError, e:
store_user_mail = None store_user_mail = None
try: update_or_insert_values = {'first_name': store_user_firstname,
# 'last_name': store_user_lastname,
'email': store_user_mail}
if '@' not in username:
# user as username # user as username
# ################# # ################
fields = ['first_name', 'last_name', 'email']
user_in_db = db(db.auth_user.username == username) user_in_db = db(db.auth_user.username == username)
if user_in_db.count() > 0: elif '@' in username:
user_in_db.update(first_name=store_user_firstname,
last_name=store_user_lastname,
email=store_user_mail)
else:
db.auth_user.insert(first_name=store_user_firstname,
last_name=store_user_lastname,
email=store_user_mail,
username=username)
except:
#
# user as email # user as email
# ############## # #############
fields = ['first_name', 'last_name']
user_in_db = db(db.auth_user.email == username) user_in_db = db(db.auth_user.email == username)
if user_in_db.count() > 0: update_or_insert_values = dict(((f, update_or_insert_values[f]) for f in fields))
user_in_db.update(first_name=store_user_firstname,
last_name=store_user_lastname) if user_in_db.count() > 0:
else: actual_values = user_in_db.select(*[db.auth_user[f] for f in fields]).first().as_dict()
db.auth_user.insert(first_name=store_user_firstname, if update_or_insert_values != actual_values: # We don't update record if values are the same
last_name=store_user_lastname, user_in_db.update(**update_or_insert_values)
email=username) else:
db.auth_user.insert(**update_or_insert_values)
con.unbind() con.unbind()
if manage_groups: if manage_groups:
@@ -478,9 +481,7 @@ def ldap_auth(server='ldap', port=None,
# No match # No match
return False return False
def do_manage_groups(username, def do_manage_groups(username, password=None, db=db):
password=None,
db=db):
""" """
Manage user groups Manage user groups
@@ -500,41 +501,43 @@ def ldap_auth(server='ldap', port=None,
# Get all group name where the user is in actually in local db # Get all group name where the user is in actually in local db
# ############################################################# # #############################################################
try: try:
db_user_id = db(db.auth_user.username == username).select( db_user_id = db(db.auth_user.username == username).select(db.auth_user.id).first().id
db.auth_user.id).first().id
except: except:
try: try:
db_user_id = db(db.auth_user.email == username).select( db_user_id = db(db.auth_user.email == username).select(db.auth_user.id).first().id
db.auth_user.id).first().id
except AttributeError, e: except AttributeError, e:
# #
# There is no user in local db # There is no user in local db
# We create one # We create one
# ############################## # ##############################
try: try:
db_user_id = db.auth_user.insert(username=username, db_user_id = db.auth_user.insert(username=username, first_name=username)
first_name=username)
except AttributeError, e: except AttributeError, e:
db_user_id = db.auth_user.insert(email=username, db_user_id = db.auth_user.insert(email=username, first_name=username)
first_name=username)
if not db_user_id: if not db_user_id:
logging.error( logging.error(
'There is no username or email for %s!' % username) 'There is no username or email for %s!' % username)
raise raise
db_group_search = db((db.auth_membership.user_id == db_user_id) & # if old pydal version, assume this is a relational database which can do joins
(db.auth_user.id == db.auth_membership.user_id) & db_can_join = db.can_join() if hasattr(db, 'can_join') else True
(db.auth_group.id == db.auth_membership.group_id)) if db_can_join:
db_group_search = \
db((db.auth_membership.user_id == db_user_id) &
(db.auth_user.id == db.auth_membership.user_id) &
(db.auth_group.id == db.auth_membership.group_id))
else:
# no joins on NoSQL databases, perform two queries
db_group_search = db(db.auth_membership.user_id == db_user_id)
group_ids = [x.group_id for x in db_group_search.select(db.auth_membership.group_id, distinct=True)]
db_group_search = db(db.auth_group.id.belongs(group_ids))
db_groups_of_the_user = list() db_groups_of_the_user = list()
db_group_id = dict() db_group_id = dict()
if db_group_search.count() > 0: if db_group_search.count() > 0:
for group in db_group_search.select(db.auth_group.id, for group in db_group_search.select(db.auth_group.id, db.auth_group.role, distinct=True):
db.auth_group.role,
distinct=True):
db_group_id[group.role] = group.id db_group_id[group.role] = group.id
db_groups_of_the_user.append(group.role) db_groups_of_the_user.append(group.role)
logging.debug('db groups of user %s: %s' % logging.debug('db groups of user %s: %s' % (username, str(db_groups_of_the_user)))
(username, str(db_groups_of_the_user)))
# #
# Delete user membership from groups where user is not anymore # Delete user membership from groups where user is not anymore
@@ -542,8 +545,7 @@ def ldap_auth(server='ldap', port=None,
for group_to_del in db_groups_of_the_user: for group_to_del in db_groups_of_the_user:
if ldap_groups_of_the_user.count(group_to_del) == 0: if ldap_groups_of_the_user.count(group_to_del) == 0:
db((db.auth_membership.user_id == db_user_id) & db((db.auth_membership.user_id == db_user_id) &
(db.auth_membership.group_id == \ (db.auth_membership.group_id == db_group_id[group_to_del])).delete()
db_group_id[group_to_del])).delete()
# #
# Create user membership in groups where user is not in already # Create user membership in groups where user is not in already
@@ -551,16 +553,12 @@ def ldap_auth(server='ldap', port=None,
for group_to_add in ldap_groups_of_the_user: for group_to_add in ldap_groups_of_the_user:
if db_groups_of_the_user.count(group_to_add) == 0: if db_groups_of_the_user.count(group_to_add) == 0:
if db(db.auth_group.role == group_to_add).count() == 0: if db(db.auth_group.role == group_to_add).count() == 0:
gid = db.auth_group.insert(role=group_to_add, gid = db.auth_group.insert(role=group_to_add, description='Generated from LDAP')
description='Generated from LDAP')
else: else:
gid = db(db.auth_group.role == group_to_add).select( gid = db(db.auth_group.role == group_to_add).select(db.auth_group.id).first().id
db.auth_group.id).first().id db.auth_membership.insert(user_id=db_user_id, group_id=gid)
db.auth_membership.insert(user_id=db_user_id,
group_id=gid)
except: except:
logger.warning("[%s] Groups are not managed successfully!" % logger.warning("[%s] Groups are not managed successfully!" % str(username))
str(username))
import traceback import traceback
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())
return False return False
@@ -600,6 +598,8 @@ def ldap_auth(server='ldap', port=None,
ldap_port = 389 ldap_port = 389
con = ldap.initialize( con = ldap.initialize(
"ldap://" + ldap_server + ":" + str(ldap_port)) "ldap://" + ldap_server + ":" + str(ldap_port))
if tls:
con.start_tls_s()
return con return con
def get_user_groups_from_ldap(username, def get_user_groups_from_ldap(username,
@@ -649,10 +649,12 @@ def ldap_auth(server='ldap', port=None,
con.simple_bind_s(username, password) con.simple_bind_s(username, password)
logger.debug('Ldap username connect...') logger.debug('Ldap username connect...')
# We have to use the full string # We have to use the full string
username = con.search_ext_s(base_dn, ldap.SCOPE_SUBTREE, username = \
"(&(sAMAccountName=%s)(%s))" % con.search_ext_s(base_dn,
(ldap.filter.escape_filter_chars(username_bare), ldap.SCOPE_SUBTREE,
filterstr), ["cn"])[0][0] "(&(sAMAccountName=%s)(%s))" % (ldap.filter.escape_filter_chars(username_bare),
filterstr),
["cn"])[0][0]
else: else:
if ldap_binddn: if ldap_binddn:
# need to search directory with an bind_dn account 1st # need to search directory with an bind_dn account 1st
@@ -665,18 +667,14 @@ def ldap_auth(server='ldap', port=None,
if username is None: if username is None:
return list() return list()
# search for groups where user is in # search for groups where user is in
filter = '(&(%s=%s)(%s))' % (ldap.filter.escape_filter_chars( filter = '(&(%s=%s)(%s))' % (ldap.filter.escape_filter_chars(group_member_attrib),
group_member_attrib
),
ldap.filter.escape_filter_chars(username), ldap.filter.escape_filter_chars(username),
group_filterstr) group_filterstr)
group_search_result = con.search_s(group_dn, group_search_result = con.search_s(group_dn, ldap.SCOPE_SUBTREE, filter, [group_name_attrib])
ldap.SCOPE_SUBTREE,
filter, [group_name_attrib])
ldap_groups_of_the_user = list() ldap_groups_of_the_user = list()
for group_row in group_search_result: for group_row in group_search_result:
group = group_row[1] group = group_row[1]
if type(group) == dict and group.has_key(group_name_attrib): if type(group) == dict and group_name_attrib in group:
ldap_groups_of_the_user.extend(group[group_name_attrib]) ldap_groups_of_the_user.extend(group[group_name_attrib])
con.unbind() con.unbind()
+21 -9
View File
@@ -139,24 +139,36 @@ server for requests. It can be used for the optional"scope" parameters for Face
Return the access token generated by the authenticating server. Return the access token generated by the authenticating server.
If token is already in the session that one will be used. If token is already in the session that one will be used.
If token has expired refresh_token is used to get another token.
Otherwise the token is fetched from the auth server. Otherwise the token is fetched from the auth server.
""" """
refresh_token = None
if current.session.token and 'expires' in current.session.token: if current.session.token and 'expires' in current.session.token:
expires = current.session.token['expires'] expires = current.session.token['expires']
# reuse token until expiration # reuse token until expiration
if expires == 0 or expires > time.time(): if expires == 0 or expires > time.time():
return current.session.token['access_token'] return current.session.token['access_token']
if 'refresh_token' in current.session.token:
refresh_token = current.session.token['refresh_token']
code = current.request.vars.code code = current.request.vars.code
if code: if code or refresh_token:
data = dict(client_id=self.client_id, data = dict(
client_secret=self.client_secret, client_id=self.client_id,
redirect_uri=current.session.redirect_uri, client_secret=self.client_secret,
code=code, )
grant_type='authorization_code' if code:
) data.update(
redirect_uri=current.session.redirect_uri,
code=code,
grant_type='authorization_code'
)
elif refresh_token:
data.update(
refresh_token=refresh_token,
grant_type='refresh_token'
)
open_url = None open_url = None
opener = self.__build_url_opener(self.token_url) opener = self.__build_url_opener(self.token_url)
+57 -4
View File
@@ -13,6 +13,7 @@ Include in your model (eg db.py)::
auth.define_tables(username=True) auth.define_tables(username=True)
from gluon.contrib.login_methods.saml2_auth import Saml2Auth from gluon.contrib.login_methods.saml2_auth import Saml2Auth
import os
auth.settings.login_form=Saml2Auth( auth.settings.login_form=Saml2Auth(
config_file = os.path.join(request.folder,'private','sp_conf'), config_file = os.path.join(request.folder,'private','sp_conf'),
maps=dict( maps=dict(
@@ -20,10 +21,59 @@ Include in your model (eg db.py)::
email=lambda v: v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0], email=lambda v: v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0],
user_id=lambda v: v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0])) user_id=lambda v: v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0]))
you must have private/sp_conf.py, the pysaml2 sp configuration file you must have private/sp_conf.py, the pysaml2 sp configuration file. For example:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
import os.path
import requests
import tempfile
BASEDIR = os.path.abspath(os.path.dirname(__file__))
# Web2py SP url and application name
HOST = 'http://127.0.0.1:8000'
APP = 'sp'
# To load the IDP metadata...
IDP_METADATA = 'http://127.0.0.1:8088/metadata'
def full_path(local_file):
return os.path.join(BASEDIR, local_file)
CONFIG = {
# your entity id, usually your subdomain plus the url to the metadata view.
'entityid': '%s/%s/default/metadata' % (HOST, APP),
'service': {
'sp' : {
'name': 'MYSP',
'endpoints': {
'assertion_consumer_service': [
('%s/%s/default/user/login' % (HOST, APP), BINDING_HTTP_REDIRECT),
('%s/%s/default/user/login' % (HOST, APP), BINDING_HTTP_POST),
],
},
},
},
# Your private and public key.
'key_file': full_path('pki/mykey.pem'),
'cert_file': full_path('pki/mycert.pem'),
# where the remote metadata is stored
'metadata': {
"remote": [{
"url": IDP_METADATA,
"cert":full_path('pki/mycert.pem')
}]
},
}
""" """
from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
from saml2.client import Saml2Client from saml2.client import Saml2Client
from gluon.utils import web2py_uuid from gluon.utils import web2py_uuid
from gluon import current, redirect, URL from gluon import current, redirect, URL
@@ -59,10 +109,13 @@ def saml2_handler(session, request, config_filename = None):
client = Saml2Client(config_file = config_filename) client = Saml2Client(config_file = config_filename)
idps = client.metadata.with_descriptor("idpsso") idps = client.metadata.with_descriptor("idpsso")
entityid = idps.keys()[0] entityid = idps.keys()[0]
bindings = [BINDING_HTTP_REDIRECT] bindings = [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]
binding, destination = client.pick_binding( binding, destination = client.pick_binding(
"single_sign_on_service", bindings, "idpsso", entity_id=entityid) "single_sign_on_service", bindings, "idpsso", entity_id=entityid)
binding = BINDING_HTTP_REDIRECT if request.env.request_method == 'GET':
binding = BINDING_HTTP_REDIRECT
elif request.env.request_method == 'POST':
binding = BINDING_HTTP_POST
if not request.vars.SAMLResponse: if not request.vars.SAMLResponse:
req_id, req = client.create_authn_request(destination, binding=binding) req_id, req = client.create_authn_request(destination, binding=binding)
relay_state = web2py_uuid().replace('-','') relay_state = web2py_uuid().replace('-','')
+53 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf8 -*- # -*- coding: utf-8 -*-
# Plural-Forms for fr (French)) # Plural-Forms for fr (French))
nplurals=2 # French language has 2 forms: nplurals=2 # French language has 2 forms:
@@ -15,3 +15,55 @@ get_plural_id = lambda n: int(n != 1)
# for words (or phrases) not found in plural_dict dictionary # for words (or phrases) not found in plural_dict dictionary
# construct_plural_form = lambda word, plural_id: (word + 'suffix') # construct_plural_form = lambda word, plural_id: (word + 'suffix')
irregular={
'aïeul': 'aïeux',
'bonhomme': 'bonshommes',
'ciel': 'cieux',
'oeil': 'yeux',
'œil': 'yeux',
'madame': 'mesdames',
'mademoiselle': 'mesdemoiselles',
'monsieur': 'messieurs',
'bijou': 'bijoux',
'caillou': 'cailloux',
'chou': 'choux',
'genou': 'genoux',
'hibou': 'hiboux',
'joujou': 'joujoux',
'pou': 'poux',
'corail': ' coraux',
'émail': 'émaux',
'travail': 'travaux',
'vitrail': 'vitraux',
'soupirail': 'soupiraux',
'bail': 'baux',
'fermail': 'fermaux',
'ventail': 'ventaux',
'bleu': 'bleus',
'pneu': 'pneus',
'émeu': 'émeus',
'enfeu': 'enfeus',
#'lieu': 'lieus', # poisson
}
def construct_plural_form(word, plural_id):
u"""
>>> [construct_plural_form(x, 1) for x in \
[ 'bleu', 'nez', 'sex', 'bas', 'gruau', 'jeu', 'journal',\
'chose' ]]
['bleus', 'nez', 'sex', 'bas', 'gruaux', 'jeux', 'journaux', 'choses']
"""
if word in irregular:
return irregular[word]
if word[-1:] in ('s', 'x', 'z'):
return word
if word[-2:] in ('au', 'eu'):
return word + 'x'
if word[-2:] == 'al':
return word[0:-2] + 'aux'
return word + 's'
if __name__ == '__main__':
import doctest
doctest.testmod()
+4 -3
View File
@@ -67,11 +67,12 @@ def RedisCache(*args, **vars):
locker.acquire() locker.acquire()
try: try:
if not hasattr(RedisCache, 'redis_instance'): instance_name = 'redis_instance_' + current.request.application
RedisCache.redis_instance = RedisClient(*args, **vars) if not hasattr(RedisCache, instance_name):
setattr(RedisCache, instance_name, RedisClient(*args, **vars))
return getattr(RedisCache, instance_name)
finally: finally:
locker.release() locker.release()
return RedisCache.redis_instance
class RedisClient(object): class RedisClient(object):
+1 -1
View File
@@ -126,7 +126,7 @@ class History:
def globals_dict(self): def globals_dict(self):
"""Returns a dictionary view of the globals. """Returns a dictionary view of the globals.
""" """
return dict((name, cPickle.loads(val)) return dict((name, pickle.loads(val))
for name, val in zip(self.global_names, self.globals)) for name, val in zip(self.global_names, self.globals))
def add_unpicklable(self, statement, names): def add_unpicklable(self, statement, names):
+1 -1
View File
@@ -33,7 +33,7 @@ except ImportError:
class JSONRPCError(RuntimeError): class JSONRPCError(RuntimeError):
"Error object for remote procedure call fail" "Error object for remote procedure call fail"
def __init__(self, code, message, data=None): def __init__(self, code, message, data=''):
value = "%s: %s\n%s" % (code, message, '\n'.join(data)) value = "%s: %s\n%s" % (code, message, '\n'.join(data))
RuntimeError.__init__(self, value) RuntimeError.__init__(self, value)
self.code = code self.code = code
+30 -8
View File
@@ -16,7 +16,7 @@ def quote(text):
class Node: class Node:
def __init__(self, name, value, url='.', readonly=False, active=True, def __init__(self, name, value, url='.', readonly=False, active=True,
onchange=None, **kwarg): onchange=None, select=False, size=4, **kwarg):
self.url = url self.url = url
self.name = name self.name = name
self.value = str(value) self.value = str(value)
@@ -26,11 +26,21 @@ class Node:
self.readonly = readonly self.readonly = readonly
self.active = active self.active = active
self.onchange = onchange self.onchange = onchange
self.size = 4 self.size = size
self.locked = False self.locked = False
self.select = value if select and not isinstance(value, str) else False
def xml(self): def xml(self):
return """<input name="%s" id="%s" value="%s" size="%s" if self.select:
selectAttributes = dict(_name=self.name,_id=self.name,_size=self.size,
_onblur="ajax('%s/blur',['%s']);"%(self.url,self.name))
# _onkeyup="ajax('%s/keyup',['%s'], ':eval');"%(self.url,self.name),
# _onfocus="ajax('%s/focus',['%s'], ':eval');"%(self.url,self.name),
for k,v in selectAttributes.items():
self.select[k] = v
return self.select.xml()
else:
return """<input name="%s" id="%s" value="%s" size="%s"
onkeyup="ajax('%s/keyup',['%s'], ':eval');" onkeyup="ajax('%s/keyup',['%s'], ':eval');"
onfocus="ajax('%s/focus',['%s'], ':eval');" onfocus="ajax('%s/focus',['%s'], ':eval');"
onblur="ajax('%s/blur',['%s'], ':eval');" %s/> onblur="ajax('%s/blur',['%s'], ':eval');" %s/>
@@ -391,7 +401,8 @@ class Sheet:
def __init__(self, rows, cols, url='.', readonly=False, def __init__(self, rows, cols, url='.', readonly=False,
active=True, onchange=None, value=None, data=None, active=True, onchange=None, value=None, data=None,
headers=None, update_button="", **kwarg): headers=None, update_button="", c_headers=None,
r_headers=None, **kwarg):
""" """
Arguments: Arguments:
@@ -425,6 +436,9 @@ class Sheet:
self.tr_attributes = {} self.tr_attributes = {}
self.td_attributes = {} self.td_attributes = {}
self.c_headers = c_headers
self.r_headers = r_headers
self.data = data self.data = data
self.readonly = readonly self.readonly = readonly
@@ -505,7 +519,7 @@ class Sheet:
self.environment[name] = obj self.environment[name] = obj
def cell(self, key, value, readonly=False, active=True, def cell(self, key, value, readonly=False, active=True,
onchange=None, **kwarg): onchange=None, select=False, **kwarg):
""" """
key is the name of the cell key is the name of the cell
value is the initial value of the cell. It can be a formula "=1+3" value is the initial value of the cell. It can be a formula "=1+3"
@@ -528,7 +542,7 @@ class Sheet:
value = value(r, c) value = value(r, c)
node = Node(key, value, self.url, readonly, active, node = Node(key, value, self.url, readonly, active,
onchange, **kwarg) onchange, select=select, **kwarg)
self.nodes[key] = node self.nodes[key] = node
self[key] = value self[key] = value
@@ -781,11 +795,19 @@ class Sheet:
gluon.html.TH, gluon.html.BR, gluon.html.SCRIPT) gluon.html.TH, gluon.html.BR, gluon.html.SCRIPT)
regex = re.compile('r\d+c\d+') regex = re.compile('r\d+c\d+')
header = TR(TH(), *[TH('c%s' % c) if not self.c_headers:
header = TR(TH(), *[TH('c%s' % c)
for c in range(self.cols)]) for c in range(self.cols)])
else:
header = TR(TH(), *[TH('%s' % c)
for c in self.c_headers])
rows = [] rows = []
for r in range(self.rows): for r in range(self.rows):
tds = [TH('r%s' % r), ] if not self.r_headers:
tds = [TH('r%s' % r), ]
else:
tds = [TH('%s' % self.r_headers[r]), ]
for c in range(self.cols): for c in range(self.cols):
key = 'r%sc%s' % (r, c) key = 'r%sc%s' % (r, c)
attributes = {"_class": "w2p_spreadsheet_col_%s" % attributes = {"_class": "w2p_spreadsheet_col_%s" %
+4
View File
@@ -145,6 +145,10 @@ class TokenHandler(tornado.web.RequestHandler):
class DistributeHandler(tornado.websocket.WebSocketHandler): class DistributeHandler(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
return True
def open(self, params): def open(self, params):
group, token, name = params.split('/') + [None, None] group, token, name = params.split('/') + [None, None]
self.group = group or 'default' self.group = group or 'default'
+2 -2
View File
@@ -7,12 +7,12 @@
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
Takes care of adapting pyDAL to web2py's needs Takes care of adapting pyDAL to web2py's needs
-------------------------------------------- -----------------------------------------------
""" """
from pydal import DAL as DAL from pydal import DAL as DAL
from pydal import Field from pydal import Field
from pydal.objects import Row, Rows, Table, Query, Expression from pydal.objects import Row, Rows, Table, Query, Set, Expression
from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon
+407
View File
@@ -0,0 +1,407 @@
import cgi
import copy_reg
from gluon import current, URL, DAL
from gluon.storage import Storage
from gluon.utils import web2py_uuid
from gluon.sanitizer import sanitize
# ################################################################
# New HTML Helpers
# ################################################################
def xmlescape(text):
return cgi.escape(text, True).replace("'", "&#x27;")
class TAG(object):
def __init__(self, name, *children, **attributes):
self.name = name
self.children = list(children)
self.attributes = attributes
for child in self.children:
if isinstance(child, TAG):
child.parent = self
def xml(self):
name = self.name
a = ' '.join('%s="%s"' %
(k[1:], k[1:] if v is True else xmlescape(unicode(v)))
for k,v in self.attributes.iteritems()
if k.startswith('_') and not v in (False,None))
if a:
a = ' '+a
if name.endswith('/'):
return '<%s%s/>' % (name, a)
else:
b = ''.join(s.xml() if isinstance(s,TAG) else xmlescape(unicode(s))
for s in self.children)
return '<%s%s>%s</%s>' %(name, a, b, name)
def __unicode__(self):
return self.xml()
def __str__(self):
return self.xml().encode('utf8')
def __getitem__(self, key):
if isinstance(key, int):
return self.children[key]
else:
return self.attributes[key]
def __setitem__(self, key, value):
if isinstance(key, int):
self.children[key] = value
else:
self.attributes[key] = value
def append(self, value):
self.children.append(value)
def __delitem__(self,key):
if isinstance(key, int):
self.children = self.children[:key]+self.children[key+1:]
else:
del self.attributes[key]
def __len__(self):
return len(self.children)
def find(self, query):
raise NotImplementedError
class METATAG(object):
def __getattr__(self, name):
return self(name)
def __call__(self, name):
return lambda *children, **attributes: TAG(name, *children, **attributes)
tag = METATAG()
DIV = tag('div')
SPAN = tag('span')
LI = tag('li')
OL = tag('ol')
UL = tag('ul')
A = tag('a')
H1 = tag('h1')
H2 = tag('h2')
H3 = tag('h3')
H4 = tag('h4')
H5 = tag('h5')
H6 = tag('h6')
EM = tag('em')
TR = tag('tr')
TD = tag('td')
TH = tag('th')
IMG = tag('img/')
FORM = tag('form')
HEAD = tag('head')
BODY = tag('body')
TABLE = tag('table')
INPUT = tag('input/')
LABEL = tag('label')
STRONG = tag('strong')
SELECT = tag('select')
OPTION = tag('option')
TEXTAREA = tag('textarea')
# ################################################################
# New XML Helpers
# ################################################################
class XML(TAG):
"""
use it to wrap a string that contains XML/HTML so that it will not be
escaped by the template
Examples:
>>> XML('<h1>Hello</h1>').xml()
'<h1>Hello</h1>'
"""
def __init__(
self,
text,
sanitize=False,
permitted_tags=[
'a','b','blockquote','br/','i','li','ol','ul','p','cite',
'code','pre','img/','h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'table', 'tr', 'td', 'div','strong', 'span'],
allowed_attributes={
'a': ['href', 'title', 'target'],
'img': ['src', 'alt'],
'blockquote': ['type'],
'td': ['colspan']},
):
"""
Args:
text: the XML text
sanitize: sanitize text using the permitted tags and allowed
attributes (default False)
permitted_tags: list of permitted tags (default: simple list of
tags)
allowed_attributes: dictionary of allowed attributed (default
for A, IMG and BlockQuote).
The key is the tag; the value is a list of allowed attributes.
"""
if sanitize:
text = sanitize(text, permitted_tags, allowed_attributes)
if isinstance(text, unicode):
text = text.encode('utf8', 'xmlcharrefreplace')
elif not isinstance(text, str):
text = str(text)
self.text = text
def xml(self):
return self.text
def __str__(self):
return self.text
def __add__(self, other):
return '%s%s' % (self, other)
def __radd__(self, other):
return '%s%s' % (other, self)
def __cmp__(self, other):
return cmp(str(self), str(other))
def __hash__(self):
return hash(str(self))
def __getitem__(self, i):
return str(self)[i]
def __getslice__(self, i, j):
return str(self)[i:j]
def __iter__(self):
for c in str(self):
yield c
def __len__(self):
return len(str(self))
def XML_unpickle(data):
return XML(marshal.loads(data))
def XML_pickle(data):
return XML_unpickle, (marshal.dumps(str(data)),)
copy_reg.pickle(XML, XML_pickle, XML_unpickle)
# ################################################################
# Simple Form Style Function (example for more complex styles)
# ################################################################
def FormStyleDefault(table, vars, errors, readonly, deletable):
form = FORM(TABLE(),_method='POST',_action='#',_enctype='multipart/form-data')
for field in table:
input_id = '%s_%s' % (field.tablename, field.name)
value = field.formatter(vars.get(field.name))
error = errors.get(field.name)
field_class = field.type.split()[0].replace(':','-')
if field.type == 'blob': # never display blobs (mistake?)
continue
elif readonly or field.type=='id':
if not field.readable:
continue
else:
control = field.represent and field.represent(value) or value or ''
elif not field.writable:
continue
elif field.widget:
control = field.widget(table, value)
elif field.type == 'text':
control = TEXTAREA(value or '', _id=input_id,_name=field.name)
elif field.type == 'boolean':
control = INPUT(_type='checkbox', _id=input_id, _name=field.name,
_value='ON', _checked = value)
elif field.type == 'upload':
control = DIV(INPUT(_type='file', _id=input_id, _name=field.name))
if value:
control.append(A('download',
_href=URL('default','download',args=value)))
control.append(INPUT(_type='checkbox',_value='ON',
_name='_delete_'+field.name))
control.append('(check to remove)')
elif hasattr(field.requires, 'options'):
multiple = field.type.startswith('list:')
value = value if isinstance(value, list) else [value]
options = [OPTION(v,_value=k,_selected=(k in value))
for k,v in field.requires.options()]
control = SELECT(*options, _id=input_id, _name=field.name,
_multiple=multiple)
else:
field_type = 'password' if field.type == 'password' else 'text'
control = INPUT(_type=field_type, _id=input_id, _name=field.name,
_value=value, _class=field_class)
form[0].append(TR(TD(LABEL(field.label,_for=input_id)),
TD(control,DIV(error,_class='error') if error else ''),
TD(field.comment or '')))
td = TD(INPUT(_type='submit',_value='Submit'))
if deletable:
td.append(INPUT(_type='checkbox',_value='ON',_name='_delete'))
td.append('(check to delete)')
form[0].append(TR(TD(),td,TD()))
return form
# ################################################################
# Form object (replaced SQLFORM)
# ################################################################
class Form(object):
"""
Usage in web2py controller:
def index():
form = Form(db.thing, record=1)
if form.accepted: ...
elif form.errors: ...
else: ...
return dict(form=form)
Arguments:
- table: a DAL table or a list of fields (equivalent to old SQLFORM.factory)
- record: a DAL record or record id
- readonly: set to True to make a readonly form
- deletable: set to False to disallow deletion of record
- formstyle: a function that renders the form using helpers (FormStyleDefault)
- dbio: set to False to prevent any DB write
- keepvalues: (NOT IMPLEMENTED)
- formname: the optional name of this form
- csrf: set to False to disable CRSF protection
"""
def __init__(self,
table,
record=None,
readonly=False,
deletable=True,
formstyle=FormStyleDefault,
dbio=True,
keepvalues=False,
formname=False,
csrf=True):
if isinstance(table, list):
dbio = False
# mimic a table from a list of fields without calling define_table
formname = formname or 'none'
for field in table: field.tablename = formname
if isinstance(record, (int, long, basestring)):
record_id = int(str(record))
self.record = table[record_id]
else:
self.record = record
self.table = table
self.readonly = readonly
self.deletable = deletable and not readonly and self.record
self.formstyle = formstyle
self.dbio = dbio
self.keepvalues = True if keepvalues or self.record else False
self.csrf = csrf
self.vars = Storage()
self.errors = Storage()
self.submitted = False
self.deleted = False
self.accepted = False
self.cached_helper = False
self.formname = formname or table._tablename
self.formkey = None
request = current.request
session = current.session
post_vars = request.post_vars
if readonly or request.env.request_method=='GET':
if self.record:
self.vars = self.record
else:
print post_vars
self.submitted = True
# check for CSRF
if csrf and self.formname in (session._formkeys or {}):
self.formkey = session._formkeys[self.formname]
# validate fields
if not csrf or post_vars._formkey == self.formkey:
if not post_vars._delete:
for field in self.table:
if field.writable:
value = post_vars.get(field.name)
(value, error) = field.validate(value)
if field.type == 'upload':
delete = post_vars.get('_delete_'+field.name)
if value is not None and hasattr(value,'file'):
value = field.store(value.file,
value.filename,
field.uploadfolder)
elif self.record and not delete:
value = self.record.get(field.name)
else:
value = None
self.vars[field.name] = value
if error:
self.errors[field.name] = error
if self.record:
self.vars.id = self.record.id
if not self.errors:
self.accepted = True
if dbio:
if self.record:
self.record.update_record(**self.vars)
else:
# warning, should we really insert if record
self.vars.id = self.table.insert(**self.vars)
elif dbio:
self.deleted = True
self.record.delete_record()
# store key for future CSRF
if csrf:
if not session._formkeys:
session._formkeys = {}
if self.formname not in session._formkeys:
session._formkeys[self.formname] = web2py_uuid()
self.formkey = session._formkeys[self.formname]
def clear():
self.vars.clear()
self.errors.clear()
for field in self.table:
self.vars[field.name] = field.default
def helper(self):
if not self.cached_helper:
cached_helper = self.formstyle(self.table,
self.vars,
self.errors,
self.readonly,
self.deletable)
if self.csrf:
cached_helper.append(INPUT(_type='hidden',_name='_formkey',
_value=self.formkey))
self.cached_helper = cached_helper
return cached_helper
def xml(self):
return self.helper().xml()
def __unicode__(self):
return self.xml()
def __str__(self):
return self.xml().encode('utf8')
if __name__=='__main__':
print(DIV(SPAN('this',STRONG('a test'),XML('1<2')),_id=1,_class="my class"))
+59 -36
View File
@@ -26,6 +26,8 @@ import gluon.settings as settings
from gluon.utils import web2py_uuid, secure_dumps, secure_loads from gluon.utils import web2py_uuid, secure_dumps, secure_loads
from gluon.settings import global_settings from gluon.settings import global_settings
from gluon import recfile from gluon import recfile
from gluon.cache import CacheInRam
from gluon.fileutils import copystream
import hashlib import hashlib
import portalocker import portalocker
try: try:
@@ -47,8 +49,7 @@ import cgi
import urlparse import urlparse
import copy import copy
import tempfile import tempfile
from gluon.cache import CacheInRam
from gluon.fileutils import copystream
FMT = '%a, %d-%b-%Y %H:%M:%S PST' FMT = '%a, %d-%b-%Y %H:%M:%S PST'
PAST = 'Sat, 1-Jan-1971 00:00:00' PAST = 'Sat, 1-Jan-1971 00:00:00'
@@ -82,13 +83,22 @@ less_template = '<link href="%s" rel="stylesheet/less" type="text/css" />'
css_inline = '<style type="text/css">\n%s\n</style>' css_inline = '<style type="text/css">\n%s\n</style>'
js_inline = '<script type="text/javascript">\n%s\n</script>' js_inline = '<script type="text/javascript">\n%s\n</script>'
template_mapping = {
'css': css_template,
'js': js_template,
'coffee': coffee_template,
'ts': typescript_template,
'less': less_template,
'css:inline': css_inline,
'js:inline': js_inline
}
# IMPORTANT: # IMPORTANT:
# this is required so that pickled dict(s) and class.__dict__ # this is required so that pickled dict(s) and class.__dict__
# are sorted and web2py can detect without ambiguity when a session changes # are sorted and web2py can detect without ambiguity when a session changes
class SortingPickler(Pickler): class SortingPickler(Pickler):
def save_dict(self, obj): def save_dict(self, obj):
self.write(EMPTY_DICT if self.bin else MARK+DICT) self.write(EMPTY_DICT if self.bin else MARK + DICT)
self.memoize(obj) self.memoize(obj)
self._batch_setitems([(key, obj[key]) for key in sorted(obj)]) self._batch_setitems([(key, obj[key]) for key in sorted(obj)])
@@ -193,11 +203,12 @@ class Request(Storage):
self.is_https = False self.is_https = False
self.is_local = False self.is_local = False
self.global_settings = settings.global_settings self.global_settings = settings.global_settings
self._uuid = None
def parse_get_vars(self): def parse_get_vars(self):
"""Takes the QUERY_STRING and unpacks it to get_vars """Takes the QUERY_STRING and unpacks it to get_vars
""" """
query_string = self.env.get('QUERY_STRING', '') query_string = self.env.get('query_string', '')
dget = urlparse.parse_qs(query_string, keep_blank_values=1) # Ref: https://docs.python.org/2/library/cgi.html#cgi.parse_qs dget = urlparse.parse_qs(query_string, keep_blank_values=1) # Ref: https://docs.python.org/2/library/cgi.html#cgi.parse_qs
get_vars = self._get_vars = Storage(dget) get_vars = self._get_vars = Storage(dget)
for (key, value) in get_vars.iteritems(): for (key, value) in get_vars.iteritems():
@@ -275,7 +286,7 @@ class Request(Storage):
""" """
self._vars = copy.copy(self.get_vars) self._vars = copy.copy(self.get_vars)
for key, value in self.post_vars.iteritems(): for key, value in self.post_vars.iteritems():
if not key in self._vars: if key not in self._vars:
self._vars[key] = value self._vars[key] = value
else: else:
if not isinstance(self._vars[key], list): if not isinstance(self._vars[key], list):
@@ -306,13 +317,21 @@ class Request(Storage):
self.parse_all_vars() self.parse_all_vars()
return self._vars return self._vars
@property
def uuid(self):
"""Lazily uuid
"""
if self._uuid is None:
self.compute_uuid()
return self._uuid
def compute_uuid(self): def compute_uuid(self):
self.uuid = '%s/%s.%s.%s' % ( self._uuid = '%s/%s.%s.%s' % (
self.application, self.application,
self.client.replace(':', '_'), self.client.replace(':', '_'),
self.now.strftime('%Y-%m-%d.%H-%M-%S'), self.now.strftime('%Y-%m-%d.%H-%M-%S'),
web2py_uuid()) web2py_uuid())
return self.uuid return self._uuid
def user_agent(self): def user_agent(self):
from gluon.contrib import user_agent_parser from gluon.contrib import user_agent_parser
@@ -436,29 +455,32 @@ class Response(Storage):
return page return page
def include_meta(self): def include_meta(self):
s = "\n"; s = "\n"
for meta in (self.meta or {}).iteritems(): for meta in (self.meta or {}).iteritems():
k, v = meta k, v = meta
if isinstance(v,dict): if isinstance(v, dict):
s = s+'<meta'+''.join(' %s="%s"' % (xmlescape(key), xmlescape(v[key])) for key in v) +' />\n' s += '<meta' + ''.join(' %s="%s"' % (xmlescape(key), xmlescape(v[key])) for key in v) +' />\n'
else: else:
s = s+'<meta name="%s" content="%s" />\n' % (k, xmlescape(v)) s += '<meta name="%s" content="%s" />\n' % (k, xmlescape(v))
self.write(s, escape=False) self.write(s, escape=False)
def include_files(self, extensions=None): def include_files(self, extensions=None):
""" """
Caching method for writing out files. Includes files (usually in the head).
Can minify and cache local files
By default, caches in ram for 5 minutes. To change, By default, caches in ram for 5 minutes. To change,
response.cache_includes = (cache_method, time_expire). response.cache_includes = (cache_method, time_expire).
Example: (cache.disk, 60) # caches to disk for 1 minute. Example: (cache.disk, 60) # caches to disk for 1 minute.
""" """
from gluon import URL
files = [] files = []
ext_files = []
has_js = has_css = False has_js = has_css = False
for item in self.files: for item in self.files:
if extensions and not item.split('.')[-1] in extensions: if isinstance(item, (list, tuple)):
ext_files.append(item)
continue
if extensions and not item.rpartition('.')[2] in extensions:
continue continue
if item in files: if item in files:
continue continue
@@ -487,10 +509,13 @@ class Response(Storage):
time_expire) time_expire)
else: else:
files = call_minify() files = call_minify()
s = ''
files.extend(ext_files)
s = []
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]
ext = f.rpartition('.')[2]
# if static_version we need also to check for # if static_version we need also to check for
# static_version_urls. In that case, the _.x.x.x # static_version_urls. In that case, the _.x.x.x
# bit would have already been added by the URL() # bit would have already been added by the URL()
@@ -498,24 +523,15 @@ class Response(Storage):
if self.static_version and not self.static_version_urls: if self.static_version and not self.static_version_urls:
item = item.replace( item = item.replace(
'/static/', '/static/_%s/' % self.static_version, 1) '/static/', '/static/_%s/' % self.static_version, 1)
if f.endswith('.css'): tmpl = template_mapping.get(ext)
s += css_template % item if tmpl:
elif f.endswith('.js'): s.append(tmpl % item)
s += js_template % item
elif f.endswith('.coffee'):
s += coffee_template % item
elif f.endswith('.ts'):
# http://www.typescriptlang.org/
s += typescript_template % item
elif f.endswith('.less'):
s += less_template % item
elif isinstance(item, (list, tuple)): elif isinstance(item, (list, tuple)):
f = item[0] f = item[0]
if f == 'css:inline': tmpl = template_mapping.get(f)
s += css_inline % item[1] if tmpl:
elif f == 'js:inline': s.append(tmpl % item[1])
s += js_inline % item[1] self.write(''.join(s), escape=False)
self.write(s, escape=False)
def stream(self, def stream(self,
stream, stream,
@@ -663,7 +679,7 @@ 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, A from gluon.html import DIV, SCRIPT, BEAUTIFY, TAG, A
BUTTON = TAG.button BUTTON = TAG.button
admin = URL("admin", "default", "design", extension='html', admin = URL("admin", "default", "design", extension='html',
args=current.request.application) args=current.request.application)
@@ -1007,10 +1023,16 @@ class Session(Storage):
def _fixup_before_save(self): def _fixup_before_save(self):
response = current.response response = current.response
rcookies = response.cookies rcookies = response.cookies
if self._forget and response.session_id_name in rcookies: scookies = rcookies.get(response.session_id_name)
if not scookies:
return
if self._forget:
del rcookies[response.session_id_name] del rcookies[response.session_id_name]
elif self._secure and response.session_id_name in rcookies: return
rcookies[response.session_id_name]['secure'] = True if self.get('httponly_cookies',True):
scookies['HttpOnly'] = True
if self._secure:
scookies['secure'] = True
def clear_session_cookies(self): def clear_session_cookies(self):
request = current.request request = current.request
@@ -1058,6 +1080,7 @@ class Session(Storage):
if response.session_storage_type == 'file': if response.session_storage_type == 'file':
target = recfile.generate(response.session_filename) target = recfile.generate(response.session_filename)
try: try:
self._close(response)
os.unlink(target) os.unlink(target)
except: except:
pass pass
+8 -3
View File
@@ -668,7 +668,7 @@ class XML(XmlComponent):
def XML_unpickle(data): def XML_unpickle(data):
return marshal.loads(data) return XML(marshal.loads(data))
def XML_pickle(data): def XML_pickle(data):
@@ -784,6 +784,9 @@ class DIV(XmlComponent):
else: else:
return self.components[i] return self.components[i]
def get(self, i):
return self.attributes.get(i)
def __setitem__(self, i, value): def __setitem__(self, i, value):
""" """
Sets attribute with name 'i' or component #i. Sets attribute with name 'i' or component #i.
@@ -1135,7 +1138,7 @@ class DIV(XmlComponent):
for (key, value) in kargs.iteritems(): for (key, value) in kargs.iteritems():
if key not in ['first_only', 'replace', 'find_text']: if key not in ['first_only', 'replace', 'find_text']:
if isinstance(value, (str, int)): if isinstance(value, (str, int)):
if self[key] != str(value): if str(self[key]) != str(value):
check = False check = False
elif key in self.attributes: elif key in self.attributes:
if not value.search(str(self[key])): if not value.search(str(self[key])):
@@ -1856,6 +1859,8 @@ class INPUT(DIV):
try: try:
(value, errors) = validator(value) (value, errors) = validator(value)
except: except:
import traceback
print traceback.format_exc()
msg = "Validation error, field:%s %s" % (name,validator) msg = "Validation error, field:%s %s" % (name,validator)
raise Exception(msg) raise Exception(msg)
if not errors is None: if not errors is None:
@@ -2643,7 +2648,7 @@ def test():
>>> form=FORM(INPUT(value="Hello World", _name="var", requires=IS_MATCH('^\w+$'))) >>> form=FORM(INPUT(value="Hello World", _name="var", requires=IS_MATCH('^\w+$')))
>>> isinstance(form.as_dict(), dict) >>> isinstance(form.as_dict(), dict)
True True
>>> form.as_dict(flat=True).has_key("vars") >>> "vars" in form.as_dict(flat=True)
True True
>>> isinstance(form.as_json(), basestring) and len(form.as_json(sanitize=False)) > 0 >>> isinstance(form.as_json(), basestring) and len(form.as_json(sanitize=False)) > 0
True True
+9 -7
View File
@@ -370,13 +370,12 @@ def wsgibase(environ, responder):
cid = env.http_web2py_component_element, cid = env.http_web2py_component_element,
is_local = (env.remote_addr in local_hosts and is_local = (env.remote_addr in local_hosts and
client == env.remote_addr), client == env.remote_addr),
is_shell = cmd_opts and cmd_opts.shell, is_shell = False,
is_sheduler = cmd_opts and cmd_opts.scheduler, is_scheduler = False,
is_https = env.wsgi_url_scheme in HTTPS_SCHEMES or \ is_https = env.wsgi_url_scheme in HTTPS_SCHEMES or \
request.env.http_x_forwarded_proto in HTTPS_SCHEMES \ request.env.http_x_forwarded_proto in HTTPS_SCHEMES \
or env.https == 'on' or env.https == 'on'
) )
request.compute_uuid() # requires client
request.url = environ['PATH_INFO'] request.url = environ['PATH_INFO']
# ################################################## # ##################################################
@@ -424,10 +423,13 @@ def wsgibase(environ, responder):
# ################################################## # ##################################################
if env.http_cookie: if env.http_cookie:
try: for single_cookie in env.http_cookie.split(';'):
request.cookies.load(env.http_cookie) single_cookie = single_cookie.strip()
except Cookie.CookieError, e: if single_cookie:
pass # invalid cookies try:
request.cookies.load(single_cookie)
except Cookie.CookieError:
pass # single invalid cookie ignore
# ################################################## # ##################################################
# try load session or create new session file # try load session or create new session file
+1 -1
View File
@@ -119,7 +119,7 @@ class LockedFile(object):
lock(self.file, LOCK_EX) lock(self.file, LOCK_EX)
if not 'a' in mode: if not 'a' in mode:
self.file.seek(0) self.file.seek(0)
self.file.truncate() self.file.truncate(0)
else: else:
raise RuntimeError("invalid LockedFile(...,mode)") raise RuntimeError("invalid LockedFile(...,mode)")
+19 -15
View File
@@ -66,14 +66,15 @@ class XssCleaner(HTMLParser):
#to strip or escape disallowed tags? #to strip or escape disallowed tags?
self.strip_disallowed = strip_disallowed self.strip_disallowed = strip_disallowed
self.in_disallowed = False # there might be data after final closing tag, that is to be ignored
self.in_disallowed = [False]
def handle_data(self, data): def handle_data(self, data):
if data and not self.in_disallowed: if data and not self.in_disallowed[-1]:
self.result += xssescape(data) self.result += xssescape(data)
def handle_charref(self, ref): def handle_charref(self, ref):
if self.in_disallowed: if self.in_disallowed[-1]:
return return
elif len(ref) < 7 and (ref.isdigit() or ref == 'x27'): # x27 is a special case for apostrophe elif len(ref) < 7 and (ref.isdigit() or ref == 'x27'): # x27 is a special case for apostrophe
self.result += '&#%s;' % ref self.result += '&#%s;' % ref
@@ -81,7 +82,7 @@ class XssCleaner(HTMLParser):
self.result += xssescape('&#%s' % ref) self.result += xssescape('&#%s' % ref)
def handle_entityref(self, ref): def handle_entityref(self, ref):
if self.in_disallowed: if self.in_disallowed[-1]:
return return
elif ref in entitydefs: elif ref in entitydefs:
self.result += '&%s;' % ref self.result += '&%s;' % ref
@@ -89,7 +90,7 @@ class XssCleaner(HTMLParser):
self.result += xssescape('&%s' % ref) self.result += xssescape('&%s' % ref)
def handle_comment(self, comment): def handle_comment(self, comment):
if self.in_disallowed: if self.in_disallowed[-1]:
return return
elif comment: elif comment:
self.result += xssescape('<!--%s-->' % comment) self.result += xssescape('<!--%s-->' % comment)
@@ -100,11 +101,11 @@ class XssCleaner(HTMLParser):
attrs attrs
): ):
if tag not in self.permitted_tags: if tag not in self.permitted_tags:
if self.strip_disallowed: self.in_disallowed.append(True)
self.in_disallowed = True if (not self.strip_disallowed):
else:
self.result += xssescape('<%s>' % tag) self.result += xssescape('<%s>' % tag)
else: else:
self.in_disallowed.append(False)
bt = '<' + tag bt = '<' + tag
if tag in self.allowed_attributes: if tag in self.allowed_attributes:
attrs = dict(attrs) attrs = dict(attrs)
@@ -119,6 +120,7 @@ class XssCleaner(HTMLParser):
else: else:
bt += ' %s=%s' % (xssescape(attribute), bt += ' %s=%s' % (xssescape(attribute),
quoteattr(attrs[attribute])) quoteattr(attrs[attribute]))
# deal with <a> without href and <img> without src
if bt == '<a' or bt == '<img': if bt == '<a' or bt == '<img':
return return
if tag in self.requires_no_close: if tag in self.requires_no_close:
@@ -129,10 +131,9 @@ class XssCleaner(HTMLParser):
def handle_endtag(self, tag): def handle_endtag(self, tag):
bracketed = '</%s>' % tag bracketed = '</%s>' % tag
self.in_disallowed.pop()
if tag not in self.permitted_tags: if tag not in self.permitted_tags:
if self.strip_disallowed: if (not self.strip_disallowed):
self.in_disallowed = False
else:
self.result += xssescape(bracketed) self.result += xssescape(bracketed)
elif tag in self.open_tags: elif tag in self.open_tags:
self.result += bracketed self.result += bracketed
@@ -143,10 +144,13 @@ class XssCleaner(HTMLParser):
Accepts relative, absolute, and mailto urls Accepts relative, absolute, and mailto urls
""" """
parsed = urlparse(url) if url.startswith('#'):
return (parsed[0] in self.allowed_schemes and '.' in parsed[1]) \ return True
or (parsed[0] in self.allowed_schemes and '@' in parsed[2]) \ else:
or (parsed[0] == '' and parsed[2].startswith('/')) parsed = urlparse(url)
return ((parsed[0] in self.allowed_schemes and '.' in parsed[1]) or
(parsed[0] in self.allowed_schemes and '@' in parsed[2]) or
(parsed[0] == '' and parsed[2].startswith('/')))
def strip(self, rawstring, escape=True): def strip(self, rawstring, escape=True):
""" """
+3 -3
View File
@@ -41,9 +41,7 @@ def enable_autocomplete_and_history(adir, env):
except ImportError: except ImportError:
pass pass
else: else:
readline.parse_and_bind("bind ^I rl_complete" readline.parse_and_bind("tab: complete")
if sys.platform == 'darwin'
else "tab: complete")
history_file = os.path.join(adir, '.pythonhistory') history_file = os.path.join(adir, '.pythonhistory')
try: try:
readline.read_history_file(history_file) readline.read_history_file(history_file)
@@ -131,6 +129,8 @@ def env(
if global_settings.cmd_options: if global_settings.cmd_options:
ip = global_settings.cmd_options.ip ip = global_settings.cmd_options.ip
port = global_settings.cmd_options.port port = global_settings.cmd_options.port
request.is_shell = global_settings.cmd_options.shell is not None
request.is_scheduler = global_settings.cmd_options.scheduler is not None
else: else:
ip, port = '127.0.0.1', '8000' ip, port = '127.0.0.1', '8000'
request.env.http_host = '%s:%s' % (ip, port) request.env.http_host = '%s:%s' % (ip, port)
+102 -73
View File
@@ -29,7 +29,7 @@ from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY
from pydal.base import DEFAULT from pydal.base import DEFAULT
from pydal.objects import Table, Row, Expression, Field from pydal.objects import Table, Row, Expression, Field
from pydal.adapters.base import CALLABLETYPES from pydal.adapters.base import CALLABLETYPES
from pydal.helpers.methods import smart_query, bar_encode from pydal.helpers.methods import smart_query, bar_encode, _repr_ref
from pydal.helpers.classes import Reference, SQLCustomType from pydal.helpers.classes import Reference, SQLCustomType
from gluon.storage import Storage from gluon.storage import Storage
from gluon.utils import md5_hash from gluon.utils import md5_hash
@@ -71,6 +71,26 @@ def represent(field, value, record):
else: else:
raise RuntimeError("field representation must take 1 or 2 args") raise RuntimeError("field representation must take 1 or 2 args")
class CacheRepresenter(object):
def __init__(self):
self.cache = {}
def __call__(self, field, value, row):
cache = self.cache
if field not in cache:
cache[field] = {}
try:
nvalue = cache[field][value]
except KeyError:
try:
nvalue = field.represent(value, row)
except KeyError:
try:
nvalue = field.represent(value, row[field.tablename])
except KeyError:
nvalue = None
if isinstance(field, _repr_ref):
cache[field][value] = nvalue
return nvalue
def safe_int(x): def safe_int(x):
try: try:
@@ -626,13 +646,12 @@ class AutocompleteWidget(object):
def __init__(self, request, field, id_field=None, db=None, def __init__(self, request, field, id_field=None, db=None,
orderby=None, limitby=(0, 10), distinct=False, orderby=None, limitby=(0, 10), distinct=False,
keyword='_autocomplete_%(tablename)s_%(fieldname)s', keyword='_autocomplete_%(tablename)s_%(fieldname)s',
min_length=2, help_fields=None, help_string=None): min_length=2, help_fields=None, help_string=None, at_beginning = True):
self.help_fields = help_fields or [] self.help_fields = help_fields or []
self.help_string = help_string self.help_string = help_string
if self.help_fields and not self.help_string: if self.help_fields and not self.help_string:
self.help_string = ' '.join('%%(%s)s' % f.name self.help_string = ' '.join('%%(%s)s' % f.name for f in self.help_fields)
for f in self.help_fields)
self.request = request self.request = request
self.keyword = keyword % dict(tablename=field.tablename, self.keyword = keyword % dict(tablename=field.tablename,
@@ -642,6 +661,7 @@ class AutocompleteWidget(object):
self.limitby = limitby self.limitby = limitby
self.distinct = distinct self.distinct = distinct
self.min_length = min_length self.min_length = min_length
self.at_beginning = at_beginning
self.fields = [field] self.fields = [field]
if id_field: if id_field:
self.is_reference = True self.is_reference = True
@@ -659,8 +679,10 @@ class AutocompleteWidget(object):
field = self.fields[0] field = self.fields[0]
if settings and settings.global_settings.web2py_runtime_gae: if settings and settings.global_settings.web2py_runtime_gae:
rows = self.db(field.__ge__(self.request.vars[self.keyword]) & field.__lt__(self.request.vars[self.keyword] + u'\ufffd')).select(orderby=self.orderby, limitby=self.limitby, *(self.fields+self.help_fields)) rows = self.db(field.__ge__(self.request.vars[self.keyword]) & field.__lt__(self.request.vars[self.keyword] + u'\ufffd')).select(orderby=self.orderby, limitby=self.limitby, *(self.fields+self.help_fields))
else: elif self.at_beginning:
rows = self.db(field.like(self.request.vars[self.keyword] + '%', case_sensitive=False)).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *(self.fields+self.help_fields)) rows = self.db(field.like(self.request.vars[self.keyword] + '%', case_sensitive=False)).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *(self.fields+self.help_fields))
else:
rows = self.db(field.contains(self.request.vars[self.keyword], case_sensitive=False)).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *(self.fields+self.help_fields))
if rows: if rows:
if self.is_reference: if self.is_reference:
id_field = self.fields[1] id_field = self.fields[1]
@@ -818,7 +840,7 @@ def formstyle_bootstrap(form, fields):
controls.add_class('span4') controls.add_class('span4')
if isinstance(label, LABEL): if isinstance(label, LABEL):
label['_class'] = 'control-label' label['_class'] = add_class(label.get('_class'),'control-label')
if _submit: if _submit:
# submit button has unwrapped label and controls, different class # submit button has unwrapped label and controls, different class
@@ -868,7 +890,7 @@ def formstyle_bootstrap3_stacked(form, fields):
e.add_class('form-control') e.add_class('form-control')
if isinstance(label, LABEL): if isinstance(label, LABEL):
label['_class'] = 'control-label' label['_class'] = add_class(label.get('_class'),'control-label')
parent.append(DIV(label, _controls, _class='form-group', _id=id)) parent.append(DIV(label, _controls, _class='form-group', _id=id))
return parent return parent
@@ -916,8 +938,10 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3):
elif isinstance(controls, UL): elif isinstance(controls, UL):
for e in controls.elements("input"): for e in controls.elements("input"):
e.add_class('form-control') e.add_class('form-control')
elif controls is None or isinstance(controls, basestring):
_controls = P(controls, _class="form-control-static %s" % col_class)
if isinstance(label, LABEL): if isinstance(label, LABEL):
label['_class'] = 'control-label %s' % label_col_class label['_class'] = add_class(label.get('_class'),'control-label %s' % label_col_class)
parent.append(DIV(label, _controls, _class='form-group', _id=id)) parent.append(DIV(label, _controls, _class='form-group', _id=id))
return parent return parent
@@ -1100,10 +1124,12 @@ class SQLFORM(FORM):
raise HTTP(404, "Object not found") raise HTTP(404, "Object not found")
self.record = record self.record = record
self.record_id = record_id
if keyed: if keyed:
self.record_id = dict([(k, record and str(record[k]) or None) self.record_id = dict([(k, record and str(record[k]) or None)
for k in table._primarykey]) for k in table._primarykey])
else:
self.record_id = record_id
self.field_parent = {} self.field_parent = {}
xfields = [] xfields = []
self.fields = fields self.fields = fields
@@ -1126,7 +1152,8 @@ class SQLFORM(FORM):
extra_fields = extra_fields or [] extra_fields = extra_fields or []
self.extra_fields = {} self.extra_fields = {}
for extra_field in extra_fields: for extra_field in extra_fields:
self.fields.append(extra_field.name) if not extra_field.name in self.fields:
self.fields.append(extra_field.name)
self.extra_fields[extra_field.name] = extra_field self.extra_fields[extra_field.name] = extra_field
extra_field.db = table._db extra_field.db = table._db
extra_field.table = table extra_field.table = table
@@ -1160,6 +1187,14 @@ class SQLFORM(FORM):
label = LABEL(label, label and sep, _for=field_id, label = LABEL(label, label and sep, _for=field_id,
_id=field_id + SQLFORM.ID_LABEL_SUFFIX) _id=field_id + SQLFORM.ID_LABEL_SUFFIX)
cond = readonly or \
(not ignore_rw and not field.writable and field.readable)
if cond:
label['_class'] = 'readonly'
else:
label['_class'] = ''
row_id = field_id + SQLFORM.ID_ROW_SUFFIX row_id = field_id + SQLFORM.ID_ROW_SUFFIX
if field.type == 'id': if field.type == 'id':
self.custom.dspval.id = nbsp self.custom.dspval.id = nbsp
@@ -1188,8 +1223,6 @@ class SQLFORM(FORM):
default = field.default default = field.default
if isinstance(default, CALLABLETYPES): if isinstance(default, CALLABLETYPES):
default = default() default = default()
cond = readonly or \
(not ignore_rw and not field.writable and field.readable)
if default is not None and not cond: if default is not None and not cond:
default = field.formatter(default) default = field.formatter(default)
@@ -1472,12 +1505,11 @@ class SQLFORM(FORM):
**kwargs **kwargs
) )
self.deleted = \ self.deleted = request_vars.get(self.FIELDNAME_REQUEST_DELETE, False)
request_vars.get(self.FIELDNAME_REQUEST_DELETE, False)
self.custom.end = CAT(self.hidden_fields(), self.custom.end) self.custom.end = CAT(self.hidden_fields(), self.custom.end)
auch = record_id and self.errors and self.deleted delete_exception = self.record_id and self.errors and self.deleted
if self.record_changed and self.detect_record_change: if self.record_changed and self.detect_record_change:
message_onchange = \ message_onchange = \
@@ -1489,8 +1521,9 @@ class SQLFORM(FORM):
if message_onchange is not None: if message_onchange is not None:
current.response.flash = message_onchange current.response.flash = message_onchange
return ret return ret
elif (not ret) and (not auch):
# auch is true when user tries to delete a record elif (not ret) and (not delete_exception):
# delete_exception is true when user tries to delete a record
# that does not pass validation, yet it should be deleted # that does not pass validation, yet it should be deleted
for fieldname in self.fields: for fieldname in self.fields:
@@ -1520,9 +1553,10 @@ class SQLFORM(FORM):
self.accepted = ret self.accepted = ret
return ret return ret
if record_id and str(record_id) != str(self.record_id): if self.record_id:
raise SyntaxError('user is tampering with form\'s record_id: ' if str(record_id) != str(self.record_id):
'%s != %s' % (record_id, self.record_id)) raise SyntaxError('user is tampering with form\'s record_id: '
'%s != %s' % (record_id, self.record_id))
if record_id and dbio and not keyed: if record_id and dbio and not keyed:
self.vars.id = self.record[self.id_field_name] self.vars.id = self.record[self.id_field_name]
@@ -1678,7 +1712,7 @@ class SQLFORM(FORM):
self.vars.update(pk) self.vars.update(pk)
else: else:
ret = False ret = False
else: elif self.table._db._uri:
if record_id: if record_id:
self.vars.id = self.record[self.id_field_name] self.vars.id = self.record[self.id_field_name]
if fields: if fields:
@@ -1686,11 +1720,13 @@ class SQLFORM(FORM):
self.id_field_name]).update(**fields) self.id_field_name]).update(**fields)
else: else:
self.vars.id = self.table.insert(**fields) self.vars.id = self.table.insert(**fields)
self.accepted = ret self.accepted = ret
return ret return ret
AUTOTYPES = { AUTOTYPES = {
type(''): ('string', None), type(''): ('string', None),
type(u''): ('string',None),
type(True): ('boolean', None), type(True): ('boolean', None),
type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)), type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)),
type(1.0): ('double', IS_FLOAT_IN_RANGE()), type(1.0): ('double', IS_FLOAT_IN_RANGE()),
@@ -1755,10 +1791,16 @@ class SQLFORM(FORM):
keywords = keywords[0] keywords = keywords[0]
request.vars.keywords = keywords request.vars.keywords = keywords
key = keywords.strip() key = keywords.strip()
if key and ' ' not in key and not '"' in key and not "'" in key: if key and not '"' in key:
SEARCHABLE_TYPES = ('string', 'text', 'list:string') SEARCHABLE_TYPES = ('string', 'text', 'list:string')
parts = [field.contains( sfields = [field for field in fields if field.type in SEARCHABLE_TYPES]
key) for field in fields if field.type in SEARCHABLE_TYPES] if settings.global_settings.web2py_runtime_gae:
return reduce(lambda a,b: a|b, [field.contains(key) for field in sfields])
else:
return reduce(lambda a,b:a&b,[
reduce(lambda a,b: a|b, [
field.contains(k) for field in sfields]
) for k in key.split()])
# from https://groups.google.com/forum/#!topic/web2py/hKe6lI25Bv4 # from https://groups.google.com/forum/#!topic/web2py/hKe6lI25Bv4
# needs testing... # needs testing...
@@ -1772,10 +1814,6 @@ class SQLFORM(FORM):
# filters.append(reduce(lambda a, b: (a & b), all_words_filters)) # filters.append(reduce(lambda a, b: (a & b), all_words_filters))
#parts = filters #parts = filters
else:
parts = None
if parts:
return reduce(lambda a, b: a | b, parts)
else: else:
return smart_query(fields, key) return smart_query(fields, key)
@@ -1838,15 +1876,19 @@ class SQLFORM(FORM):
operators = SELECT(*[OPTION(T(option), _value=option) for option in options], _class='form-control') operators = SELECT(*[OPTION(T(option), _value=option) for option in options], _class='form-control')
_id = "%s_%s" % (value_id, name) _id = "%s_%s" % (value_id, name)
if field_type in ['boolean', 'double', 'time', 'integer']: if field_type in ['boolean', 'double', 'time', 'integer']:
value_input = SQLFORM.widgets[field_type].widget(field, field.default, _id=_id, _class='form-control') widget_ = SQLFORM.widgets[field_type]
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control')
elif field_type == 'date': elif field_type == 'date':
iso_format = {'_data-w2p_date_format' : '%Y-%m-%d'} iso_format = {'_data-w2p_date_format': '%Y-%m-%d'}
value_input = SQLFORM.widgets.date.widget(field, field.default, _id=_id, _class='form-control', **iso_format) widget_ = SQLFORM.widgets.date
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format)
elif field_type == 'datetime': elif field_type == 'datetime':
iso_format = {'_data-w2p_datetime_format' : '%Y-%m-%d %H:%M:%S'} iso_format = {'_data-w2p_datetime_format': '%Y-%m-%d %H:%M:%S'}
value_input = SQLFORM.widgets.datetime.widget(field, field.default, _id=_id, _class='form-control', **iso_format) widget_ = SQLFORM.widgets.datetime
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format)
elif (field_type.startswith('reference ') or elif (field_type.startswith('reference ') or
field_type.startswith('list:reference ')) and \ field_type.startswith('list:reference ')) and \
hasattr(field.requires, 'options') or \
hasattr(field.requires, 'options'): hasattr(field.requires, 'options'):
value_input = SELECT( value_input = SELECT(
*[OPTION(v, _value=k) *[OPTION(v, _value=k)
@@ -1856,7 +1898,8 @@ class SQLFORM(FORM):
elif field_type.startswith('reference ') or \ elif field_type.startswith('reference ') or \
field_type.startswith('list:integer') or \ field_type.startswith('list:integer') or \
field_type.startswith('list:reference '): field_type.startswith('list:reference '):
value_input = SQLFORM.widgets.integer.widget(field, field.default, _id=_id, _class='form-control') widget_ = SQLFORM.widgets.integer
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control')
else: else:
value_input = INPUT( value_input = INPUT(
_type='text', _id=_id, _type='text', _id=_id,
@@ -1967,7 +2010,8 @@ class SQLFORM(FORM):
cache_count=None, cache_count=None,
client_side_delete=False, client_side_delete=False,
ignore_common_filters=None, ignore_common_filters=None,
auto_pagination=True): auto_pagination=True,
use_cursor=False):
formstyle = formstyle or current.response.formstyle formstyle = formstyle or current.response.formstyle
@@ -2039,7 +2083,7 @@ class SQLFORM(FORM):
## if it's not an integer ## if it's not an integer
if cache_count is None or isinstance(cache_count, tuple): if cache_count is None or isinstance(cache_count, tuple):
if groupby: if groupby:
c = 'count(*)' c = 'count(*) AS count_all'
nrows = db.executesql( nrows = db.executesql(
'select count(*) from (%s) _tmp;' % 'select count(*) from (%s) _tmp;' %
dbset._select(c, left=left, cacheable=True, dbset._select(c, left=left, cacheable=True,
@@ -2069,18 +2113,15 @@ class SQLFORM(FORM):
# is unique and usually indexed. See issue #679 # is unique and usually indexed. See issue #679
if not orderby: if not orderby:
orderby = field_id orderby = field_id
else: elif isinstance(orderby, list):
if isinstance(orderby, Expression): orderby = reduce(lambda a,b: a|b, orderby)
if orderby.first: elif isinstance(orderby, Field) and orderby is not field_id:
# here we're with a DESC order on a field # here we're with an ASC order on a field stored as orderby
# stored as orderby.first orderby = orderby | field_id
if orderby.first is not field_id: elif (isinstance(orderby, Expression) and
orderby = orderby | field_id orderby.first and orderby.first is not field_id):
else: # here we're with a DESC order on a field stored as orderby.first
# here we're with an ASC order on a field orderby = orderby | field_id
# stored as orderby
if orderby is not field_id:
orderby = orderby | field_id
return orderby return orderby
def url(**b): def url(**b):
@@ -2108,10 +2149,8 @@ class SQLFORM(FORM):
# - url has valid signature (vars are not signed, only path_info) # - url has valid signature (vars are not signed, only path_info)
# = url does not contain 'create','delete','edit' (readonly) # = url does not contain 'create','delete','edit' (readonly)
if user_signature: if user_signature:
if not ( if not ('/'.join(map(str,args)) == '/'.join(map(str,request.args)) or
'/'.join(str(a) for a in args) == '/'.join(request.args) or URL.verify(request, user_signature=user_signature, hash_vars=False) or
URL.verify(request, user_signature=user_signature,
hash_vars=False) or
(request.args(len(args)) == 'view' and not logged)): (request.args(len(args)) == 'view' and not logged)):
session.flash = T('not authorized') session.flash = T('not authorized')
redirect(referrer) redirect(referrer)
@@ -2542,7 +2581,7 @@ class SQLFORM(FORM):
cursor = True cursor = True
# figure out what page we are one to setup the limitby # figure out what page we are one to setup the limitby
if paginate and dbset._db._adapter.dbengine == 'google:datastore': if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor:
cursor = request.vars.cursor or True cursor = request.vars.cursor or True
limitby = (0, paginate) limitby = (0, paginate)
try: try:
@@ -2564,7 +2603,7 @@ class SQLFORM(FORM):
table_fields = [field for field in fields table_fields = [field for field in fields
if (field.tablename in tablenames and if (field.tablename in tablenames and
not(isinstance(field, Field.Virtual)))] not(isinstance(field, Field.Virtual)))]
if dbset._db._adapter.dbengine == 'google:datastore': if dbset._db._adapter.dbengine == 'google:datastore' and use_cursor:
rows = dbset.select(left=left, orderby=orderby, rows = dbset.select(left=left, orderby=orderby,
groupby=groupby, limitby=limitby, groupby=groupby, limitby=limitby,
reusecursor=cursor, reusecursor=cursor,
@@ -2574,6 +2613,7 @@ class SQLFORM(FORM):
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) cacheable=True, *table_fields)
next_cursor = None
except SyntaxError: except SyntaxError:
rows = None rows = None
next_cursor = None next_cursor = None
@@ -2592,7 +2632,7 @@ class SQLFORM(FORM):
console.append(DIV(message or '', _class='web2py_counter')) console.append(DIV(message or '', _class='web2py_counter'))
paginator = UL() paginator = UL()
if paginate and dbset._db._adapter.dbengine == 'google:datastore': if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor:
# this means we may have a large table with an unknown number of rows. # this means we may have a large table with an unknown number of rows.
try: try:
page = int(request.vars.page or 1) - 1 page = int(request.vars.page or 1) - 1
@@ -2663,7 +2703,7 @@ class SQLFORM(FORM):
htmltable = TABLE(COLGROUP(*cols), THEAD(head)) htmltable = TABLE(COLGROUP(*cols), THEAD(head))
tbody = TBODY() tbody = TBODY()
numrec = 0 numrec = 0
repr_cache = {} repr_cache = CacheRepresenter()
for row in rows: for row in rows:
trcols = [] trcols = []
id = row[field_id] id = row[field_id]
@@ -2676,31 +2716,20 @@ class SQLFORM(FORM):
continue continue
if field.type == 'blob': if field.type == 'blob':
continue continue
value = row[str(field)] if isinstance(field, Field.Virtual) and field.tablename in row:
value = dbset.db[field.tablename][row[field.tablename][field_id]][field.name]
else:
value = row[str(field)]
maxlength = maxtextlengths.get(str(field), maxtextlength) maxlength = maxtextlengths.get(str(field), maxtextlength)
if field.represent: if field.represent:
if field.type.startswith('reference'): if field.type.startswith('reference'):
if field not in repr_cache: nvalue = repr_cache(field, value, row)
repr_cache[field] = {}
try:
nvalue = repr_cache[field][value]
except KeyError:
try:
nvalue = field.represent(value, row)
except KeyError:
try:
nvalue = field.represent(
value, row[field.tablename])
except KeyError:
nvalue = None
repr_cache[field][value] = nvalue
else: else:
try: try:
nvalue = field.represent(value, row) nvalue = field.represent(value, row)
except KeyError: except KeyError:
try: try:
nvalue = field.represent( nvalue = field.represent(value, row[field.tablename])
value, row[field.tablename])
except KeyError: except KeyError:
nvalue = None nvalue = None
value = nvalue value = nvalue
+31 -28
View File
@@ -269,31 +269,33 @@ class FastStorage(dict):
class List(list): class List(list):
""" """
Like a regular python list but a[i] if i is out of bounds returns None Like a regular python list but callable.
instead of `IndexOutOfBounds` When a(i) is called if i is out of bounds returns None
instead of `IndexError`.
""" """
def __call__(self, i, default=DEFAULT, cast=None, otherwise=None): def __call__(self, i, default=DEFAULT, cast=None, otherwise=None):
"""Allows to use a special syntax for fast-check of `request.args()` """Allows to use a special syntax for fast-check of
validity `request.args()` validity.
:params:
Args:
i: index i: index
default: use this value if arg not found default: use this value if arg not found
cast: type cast cast: type cast
otherwise: can be: otherwise:
will be executed when:
- None: results in a 404 - casts fail
- str: redirect to this address - value not found, dont have default and otherwise is
- callable: calls the function (nothing is passed) especified
can be:
- None: results in a 404
- str: redirect to this address
- callable: calls the function (nothing is passed)
Example: Example:
You can use:: You can use::
request.args(0,default=0,cast=int,otherwise='http://error_url') request.args(0,default=0,cast=int,otherwise='http://error_url')
request.args(0,default=0,cast=int,otherwise=lambda:...) request.args(0,default=0,cast=int,otherwise=lambda:...)
""" """
n = len(self) n = len(self)
if 0 <= i < n or -n <= i < 0: if 0 <= i < n or -n <= i < 0:
@@ -301,23 +303,24 @@ class List(list):
elif default is DEFAULT: elif default is DEFAULT:
value = None value = None
else: else:
value, cast = default, False value, cast, otherwise = default, False, False
if cast: try:
try: if cast:
value = cast(value) value = cast(value)
except (ValueError, TypeError): if not value and otherwise:
from http import HTTP, redirect raise ValueError('Otherwise will raised.')
if otherwise is None: except (ValueError, TypeError):
raise HTTP(404) from http import HTTP, redirect
elif isinstance(otherwise, str): if otherwise is None:
redirect(otherwise) raise HTTP(404)
elif callable(otherwise): elif isinstance(otherwise, str):
return otherwise() redirect(otherwise)
else: elif callable(otherwise):
raise RuntimeError("invalid otherwise") return otherwise()
else:
raise RuntimeError("invalid otherwise")
return value return value
if __name__ == '__main__': if __name__ == '__main__':
import doctest import doctest
doctest.testmod() doctest.testmod()
+3
View File
@@ -898,6 +898,9 @@ def render(content="hello world",
if not 'NOESCAPE' in context: if not 'NOESCAPE' in context:
context['NOESCAPE'] = NOESCAPE context['NOESCAPE'] = NOESCAPE
if isinstance(content, unicode):
content = content.encode('utf8')
# save current response class # save current response class
if context and 'response' in context: if context and 'response' in context:
old_response_body = context['response'].body old_response_body = context['response'].body
+1
View File
@@ -4,6 +4,7 @@ from test_http import *
from test_cache import * from test_cache import *
from test_contenttype import * from test_contenttype import *
from test_fileutils import * from test_fileutils import *
from test_globals import *
from test_html import * from test_html import *
from test_is_url import * from test_is_url import *
from test_languages import * from test_languages import *
+1
View File
@@ -26,6 +26,7 @@ exclude_lines =
ignore_errors = True ignore_errors = True
omit = gluon/contrib/* omit = gluon/contrib/*
gluon/tests/* gluon/tests/*
gluon/packages/*
[html] [html]
directory = coverage_html_report directory = coverage_html_report
+34 -1
View File
@@ -13,6 +13,7 @@ fix_sys_path(__file__)
from storage import Storage from storage import Storage
from cache import CacheInRam, CacheOnDisk, Cache from cache import CacheInRam, CacheOnDisk, Cache
from gluon.dal import DAL, Field
oldcwd = None oldcwd = None
@@ -30,6 +31,11 @@ def tearDownModule():
if oldcwd: if oldcwd:
os.chdir(oldcwd) os.chdir(oldcwd)
oldcwd = None oldcwd = None
try:
os.unlink('dummy.db')
except:
pass
class TestCache(unittest.TestCase): class TestCache(unittest.TestCase):
@@ -107,7 +113,34 @@ class TestCache(unittest.TestCase):
cache.clear(regex=r'a*') cache.clear(regex=r'a*')
self.assertEqual(cache('a1', lambda: 2, 0), 2) self.assertEqual(cache('a1', lambda: 2, 0), 2)
self.assertEqual(cache('a2', lambda: 3, 100), 3) self.assertEqual(cache('a2', lambda: 3, 100), 3)
return
def testDALcache(self):
s = Storage({'application': 'admin',
'folder': 'applications/admin'})
cache = Cache(s)
db = DAL(check_reserved=['all'])
db.define_table('t_a', Field('f_a'))
db.t_a.insert(f_a='test')
db.commit()
a = db(db.t_a.id > 0).select(cache=(cache.ram, 60), cacheable=True)
b = db(db.t_a.id > 0).select(cache=(cache.ram, 60), cacheable=True)
self.assertEqual(a.as_csv(), b.as_csv())
c = db(db.t_a.id > 0).select(cache=(cache.disk, 60), cacheable=True)
d = db(db.t_a.id > 0).select(cache=(cache.disk, 60), cacheable=True)
self.assertEqual(c.as_csv(), d.as_csv())
self.assertEqual(a.as_csv(), c.as_csv())
self.assertEqual(b.as_csv(), d.as_csv())
e = db(db.t_a.id > 0).select(cache=(cache.disk, 60))
f = db(db.t_a.id > 0).select(cache=(cache.disk, 60))
self.assertEqual(e.as_csv(), f.as_csv())
self.assertEqual(a.as_csv(), f.as_csv())
g = db(db.t_a.id > 0).select(cache=(cache.ram, 60))
h = db(db.t_a.id > 0).select(cache=(cache.ram, 60))
self.assertEqual(g.as_csv(), h.as_csv())
self.assertEqual(a.as_csv(), h.as_csv())
db.t_a.drop()
db.close()
if __name__ == '__main__': if __name__ == '__main__':
setUpModule() # pre-python-2.7 setUpModule() # pre-python-2.7
+85 -2
View File
@@ -4,16 +4,25 @@
Unit tests for gluon.dal Unit tests for gluon.dal
""" """
import sys
import os
import unittest import unittest
from fix_path import fix_sys_path from fix_path import fix_sys_path
fix_sys_path(__file__) fix_sys_path(__file__)
from gluon.dal import DAL, Field from gluon.dal import DAL, Field
def tearDownModule():
try:
os.unlink('dummy.db')
except:
pass
class TestDALSubclass(unittest.TestCase): class TestDALSubclass(unittest.TestCase):
def testRun(self): def testRun(self):
from gluon.serializers import custom_json, xml from gluon.serializers import custom_json, xml
from gluon import sqlhtml from gluon import sqlhtml
@@ -22,19 +31,93 @@ class TestDALSubclass(unittest.TestCase):
self.assertEqual(db.serializers['xml'], xml) self.assertEqual(db.serializers['xml'], xml)
self.assertEqual(db.representers['rows_render'], sqlhtml.represent) self.assertEqual(db.representers['rows_render'], sqlhtml.represent)
self.assertEqual(db.representers['rows_xml'], sqlhtml.SQLTABLE) self.assertEqual(db.representers['rows_xml'], sqlhtml.SQLTABLE)
db.close()
def testSerialization(self): def testSerialization(self):
import pickle import pickle
db = DAL(check_reserved=['all']) db = DAL(check_reserved=['all'])
db.define_table('t_a', Field('f_a')) db.define_table('t_a', Field('f_a'))
db.t_a.insert(f_a='test') db.t_a.insert(f_a='test')
a = db(db.t_a.id>0).select(cacheable=True) a = db(db.t_a.id > 0).select(cacheable=True)
s = pickle.dumps(a) s = pickle.dumps(a)
b = pickle.loads(s) b = pickle.loads(s)
self.assertEqual(a.db, b.db) self.assertEqual(a.db, b.db)
db.t_a.drop()
db.close()
""" TODO: """ TODO:
class TestDefaultValidators(unittest.TestCase): class TestDefaultValidators(unittest.TestCase):
def testRun(self): def testRun(self):
pass pass
""" """
def _prepare_exec_for_file(filename):
module = []
if filename.endswith('.py'):
filename = filename[:-3]
elif os.path.split(filename)[1] == '__init__.py':
filename = os.path.dirname(filename)
else:
raise 'The file provided (%s) does is not a valid Python file.'
filename = os.path.realpath(filename)
dirpath = filename
while 1:
dirpath, extra = os.path.split(dirpath)
module.append(extra)
if not os.path.isfile(os.path.join(dirpath, '__init__.py')):
break
sys.path.insert(0, dirpath)
return '.'.join(module[::-1])
def load_pydal_tests_module():
path = os.path.dirname(os.path.abspath(__file__))
if not os.path.isfile(os.path.join(path, 'web2py.py')):
i = 0
while i < 10:
i += 1
if os.path.exists(os.path.join(path, 'web2py.py')):
break
path = os.path.abspath(os.path.join(path, '..'))
pydal_test_path = os.path.join(
path, "gluon", "packages", "dal", "tests", "__init__.py")
mname = _prepare_exec_for_file(pydal_test_path)
mod = __import__(mname)
return mod
def pydal_suite():
mod = load_pydal_tests_module()
suite = unittest.TestSuite()
tlist = [
getattr(mod, el) for el in mod.__dict__.keys() if el.startswith("Test")
]
for t in tlist:
suite.addTest(unittest.makeSuite(t))
return suite
class TestDALAdapters(unittest.TestCase):
def _run_tests(self):
suite = pydal_suite()
return unittest.TextTestRunner(verbosity=2).run(suite)
def test_mysql(self):
if os.environ.get('APPVEYOR'):
return
os.environ["DB"] = "mysql://root:@localhost/pydal"
result = self._run_tests()
self.assertTrue(result)
def test_pg8000(self):
if os.environ.get('APPVEYOR'):
return
os.environ["DB"] = "postgres:pg8000://postgres:@localhost/pydal"
result = self._run_tests()
self.assertTrue(result)
if __name__ == '__main__':
unittest.main()
tearDownModule()
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Unit tests for gluon.globals
"""
import re
import unittest
from fix_path import fix_sys_path
fix_sys_path(__file__)
from gluon.globals import Request, Response, Session
from gluon import URL
def setup_clean_session():
request = Request(env={})
request.application = 'a'
request.controller = 'c'
request.function = 'f'
request.folder = 'applications/admin'
response = Response()
session = Session()
session.connect(request, response)
from gluon.globals import current
current.request = request
current.response = response
current.session = session
return current
class testResponse(unittest.TestCase):
#port from python 2.7, needed for 2.5 and 2.6 tests
def assertRegexpMatches(self, text, expected_regexp, msg=None):
"""Fail the test unless the text matches the regular expression."""
if isinstance(expected_regexp, basestring):
expected_regexp = re.compile(expected_regexp)
if not expected_regexp.search(text):
msg = msg or "Regexp didn't match"
msg = '%s: %r not found in %r' % (
msg, expected_regexp.pattern, text)
raise self.failureException(msg)
def test_include_files(self):
def return_includes(response, extensions=None):
response.include_files(extensions)
return response.body.getvalue()
response = Response()
response.files.append(URL('a', 'static', 'css/file.css'))
content = return_includes(response)
self.assertEqual(content, '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
response = Response()
response.files.append(URL('a', 'static', 'css/file.js'))
content = return_includes(response)
self.assertEqual(content, '<script src="/a/static/css/file.js" type="text/javascript"></script>')
response = Response()
response.files.append(URL('a', 'static', 'css/file.coffee'))
content = return_includes(response)
self.assertEqual(content, '<script src="/a/static/css/file.coffee" type="text/coffee"></script>')
response = Response()
response.files.append(URL('a', 'static', 'css/file.ts'))
content = return_includes(response)
self.assertEqual(content, '<script src="/a/static/css/file.ts" type="text/typescript"></script>')
response = Response()
response.files.append(URL('a', 'static', 'css/file.less'))
content = return_includes(response)
self.assertEqual(content, '<link href="/a/static/css/file.less" rel="stylesheet/less" type="text/css" />')
response = Response()
response.files.append(('css:inline', 'background-color; white;'))
content = return_includes(response)
self.assertEqual(content, '<style type="text/css">\nbackground-color; white;\n</style>')
response = Response()
response.files.append(('js:inline', 'alert("hello")'))
content = return_includes(response)
self.assertEqual(content, '<script type="text/javascript">\nalert("hello")\n</script>')
response = Response()
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js')
content = return_includes(response)
self.assertEqual(content, '<script src="https://code.jquery.com/jquery-1.11.3.min.js" type="text/javascript"></script>')
response = Response()
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0')
content = return_includes(response)
self.assertEqual(content, '<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>')
response = Response()
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0')
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0')
response.files.append(URL('a', 'static', 'css/file.css'))
response.files.append(URL('a', 'static', 'css/file.css'))
content = return_includes(response)
self.assertEqual(content,
'<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
'<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
response = Response()
response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0')
response.files.append(URL('a', 'static', 'css/file.css'))
response.files.append(URL('a', 'static', 'css/file.ts'))
content = return_includes(response)
self.assertEqual(content,
'<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
'<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />' +
'<script src="/a/static/css/file.ts" type="text/typescript"></script>' +
'<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>'
)
response = Response()
response.files.append(URL('a', 'static', 'css/file.js'))
response.files.append(URL('a', 'static', 'css/file.css'))
content = return_includes(response, extensions=['css'])
self.assertEqual(content, '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
#regr test for #628
response = Response()
response.files.append('http://maps.google.com/maps/api/js?sensor=false')
content = return_includes(response)
self.assertEqual(content, '')
#regr test for #628
response = Response()
response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
content = return_includes(response)
self.assertEqual(content, '<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>')
response = Response()
response.files.append(['js', 'http://maps.google.com/maps/api/js?sensor=false'])
content = return_includes(response)
self.assertEqual(content, '<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>')
response = Response()
response.files.append(('js1', 'http://maps.google.com/maps/api/js?sensor=false'))
content = return_includes(response)
self.assertEqual(content, '')
def test_cookies(self):
current = setup_clean_session()
cookie = str(current.response.cookies)
session_key='%s=%s'%(current.response.session_id_name,current.response.session_id)
self.assertRegexpMatches(cookie, r'^Set-Cookie: ')
self.assertTrue(session_key in cookie)
self.assertTrue('Path=/' in cookie)
def test_cookies_secure(self):
current = setup_clean_session()
current.session._fixup_before_save()
cookie = str(current.response.cookies)
self.assertTrue('secure' not in cookie)
current = setup_clean_session()
current.session.secure()
current.session._fixup_before_save()
cookie = str(current.response.cookies)
self.assertTrue('secure' in cookie)
def test_cookies_httponly(self):
current = setup_clean_session()
current.session._fixup_before_save()
cookie = str(current.response.cookies)
self.assertTrue('httponly' in cookie)
current = setup_clean_session()
current.session.httponly_cookies = True
current.session._fixup_before_save()
cookie = str(current.response.cookies)
self.assertTrue('httponly' in cookie)
current = setup_clean_session()
current.session.httponly_cookies = False
current.session._fixup_before_save()
cookie = str(current.response.cookies)
self.assertTrue('httponly' not in cookie)
if __name__ == '__main__':
unittest.main()
+18
View File
@@ -120,6 +120,7 @@ class TestStorageList(unittest.TestCase):
class TestList(unittest.TestCase): class TestList(unittest.TestCase):
""" Tests Storage.List (fast-check for request.args()) """ """ Tests Storage.List (fast-check for request.args()) """
def test_listcall(self): def test_listcall(self):
@@ -134,6 +135,23 @@ class TestList(unittest.TestCase):
self.assertEqual(a(3, cast=int), 1234) self.assertEqual(a(3, cast=int), 1234)
a.append('x') a.append('x')
self.assertRaises(HTTP, a, 4, cast=int) self.assertRaises(HTTP, a, 4, cast=int)
b = List()
# default is always returned when especified
self.assertEqual(b(0, cast=int, default=None), None)
self.assertEqual(b(0, cast=int, default=None, otherwise='teste'), None)
self.assertEqual(b(0, cast=int, default='a', otherwise='teste'), 'a')
# if don't have value and otherwise is especified it will called
self.assertEqual(b(0, otherwise=lambda: 'something'), 'something')
self.assertEqual(b(0, cast=int, otherwise=lambda: 'something'),
'something')
# except if default is especified
self.assertEqual(b(0, default=0, otherwise=lambda: 'something'), 0)
def test_listgetitem(self):
'''Mantains list behaviour.'''
a = List((1, 2, 3))
self.assertEqual(a[0], 1)
self.assertEqual(a[::-1], [3, 2, 1])
if __name__ == '__main__': if __name__ == '__main__':
+1081 -265
View File
File diff suppressed because it is too large Load Diff
+105 -59
View File
@@ -22,7 +22,7 @@ import decimal
import unicodedata import unicodedata
from cStringIO import StringIO from cStringIO import StringIO
from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE
from pydal.objects import FieldVirtual, FieldMethod from pydal.objects import Field, FieldVirtual, FieldMethod
regex_isint = re.compile('^[+-]?\d+$') regex_isint = re.compile('^[+-]?\d+$')
@@ -200,8 +200,11 @@ class IS_MATCH(Validator):
self.is_unicode = is_unicode self.is_unicode = is_unicode
def __call__(self, value): def __call__(self, value):
if self.is_unicode and not isinstance(value, unicode): if self.is_unicode:
match = self.regex.search(str(value).decode('utf8')) if isinstance(value,unicode):
match = self.regex.search(value)
else:
match = self.regex.search(str(value).decode('utf8'))
else: else:
match = self.regex.search(str(value)) match = self.regex.search(str(value))
if match is not None: if match is not None:
@@ -506,34 +509,44 @@ class IS_IN_DB(Validator):
zero='', zero='',
sort=False, sort=False,
_and=None, _and=None,
left=None left=None,
delimiter=None,
auto_add=False,
): ):
from pydal.objects import Table from pydal.objects import Table
if isinstance(field, Table):
field = field._id
if hasattr(dbset, 'define_table'): if hasattr(dbset, 'define_table'):
self.dbset = dbset() self.dbset = dbset()
else: else:
self.dbset = dbset self.dbset = dbset
if isinstance(field, Table):
field = field._id
elif isinstance(field, str):
items = field.split('.')
if len(items)==1: items+=['id']
field = self.dbset.db[items[0]][items[1]]
(ktable, kfield) = str(field).split('.') (ktable, kfield) = str(field).split('.')
if not label: if not label:
label = '%%(%s)s' % kfield label = '%%(%s)s' % kfield
if isinstance(label, str): if isinstance(label, str):
if regex1.match(str(label)): if regex1.match(str(label)):
label = '%%(%s)s' % str(label).split('.')[-1] label = '%%(%s)s' % str(label).split('.')[-1]
ks = regex2.findall(label) fieldnames = regex2.findall(label)
if kfield not in ks: if kfield not in fieldnames:
ks += [kfield] fieldnames.append(kfield) # kfield must be last
fields = ks elif isinstance(label, Field):
fieldnames = [label.name, kfield] # kfield must be last
label = '%%(%s)s' % label.name
elif callable(label):
fieldnames = '*'
else: else:
ks = [kfield] raise NotImplementedError
fields = 'all' self.field = field # the lookup field
self.fields = fields self.fieldnames = fieldnames # fields requires to build the formatting
self.label = label self.label = label
self.ktable = ktable self.ktable = ktable
self.kfield = kfield self.kfield = kfield
self.ks = ks
self.error_message = error_message self.error_message = error_message
self.theset = None self.theset = None
self.orderby = orderby self.orderby = orderby
@@ -545,6 +558,8 @@ class IS_IN_DB(Validator):
self.sort = sort self.sort = sort
self._and = _and self._and = _and
self.left = left self.left = left
self.delimiter = delimiter
self.auto_add = auto_add
def set_self_id(self, id): def set_self_id(self, id):
if self._and: if self._and:
@@ -552,10 +567,10 @@ class IS_IN_DB(Validator):
def build_set(self): def build_set(self):
table = self.dbset.db[self.ktable] table = self.dbset.db[self.ktable]
if self.fields == 'all': if self.fieldnames == '*':
fields = [f for f in table] fields = [f for f in table]
else: else:
fields = [table[k] for k in self.fields] fields = [table[k] for k in self.fieldnames]
ignore = (FieldVirtual, FieldMethod) ignore = (FieldVirtual, FieldMethod)
fields = filter(lambda f: not isinstance(f, ignore), fields) fields = filter(lambda f: not isinstance(f, ignore), fields)
if self.dbset.db._dbname != 'gae': if self.dbset.db._dbname != 'gae':
@@ -588,18 +603,42 @@ class IS_IN_DB(Validator):
items.insert(0, ('', self.zero)) items.insert(0, ('', self.zero))
return items return items
def maybe_add(self, table, fieldname, value):
d = {fieldname: value}
record = table(**d)
if record:
return record.id
else:
return table.insert(**d)
def __call__(self, value): def __call__(self, value):
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: if self._and:
raise NotImplementedError raise NotImplementedError
if isinstance(value, list): if isinstance(value, list):
values = value values = value
elif self.delimiter:
values = value.split(self.delimiter) # because of autocomplete
elif value: elif value:
values = [value] values = [value]
else: else:
values = [] values = []
if self.field.type in ('id','integer'):
new_values = []
for value in values:
if isinstance(value,(int,long)) or value.isdigit():
value = int(value)
elif self.auto_add:
value = self.maybe_add(table, self.fieldnames[0], value)
else:
return (values, translate(self.error_message))
new_values.append(value)
values = new_values
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))
@@ -618,18 +657,32 @@ class IS_IN_DB(Validator):
return (values, None) return (values, None)
elif count(values) == len(values): elif count(values) == len(values):
return (values, None) return (values, None)
elif self.theset:
if str(value) in self.theset:
if self._and:
return self._and(value)
else:
return (value, None)
else: else:
if self.dbset(field == value).count(): if self.field.type in ('id','integer'):
if self._and: if isinstance(value,(int,long)) or value.isdigit():
return self._and(value) value = int(value)
elif self.auto_add:
value = self.maybe_add(table, self.fieldnames[0], value)
else: else:
return (value, None) return (value, translate(self.error_message))
try:
value = int(value)
except TypeError:
return (values, translate(self.error_message))
if self.theset:
if str(value) in self.theset:
if self._and:
return self._and(value)
else:
return (value, None)
else:
if self.dbset(field == value).count():
if self._and:
return self._and(value)
else:
return (value, None)
return (value, translate(self.error_message)) return (value, translate(self.error_message))
@@ -691,7 +744,7 @@ class IS_NOT_IN_DB(Validator):
return (value, translate(self.error_message)) return (value, translate(self.error_message))
else: else:
row = subset.select(table._id, field, limitby=(0, 1), orderby_on_limitby=False).first() row = subset.select(table._id, field, limitby=(0, 1), orderby_on_limitby=False).first()
if row and str(row.id) != str(id): if row and str(row[table._id]) != str(id):
return (value, translate(self.error_message)) return (value, translate(self.error_message))
return (value, None) return (value, None)
@@ -2162,29 +2215,22 @@ class IS_DATE(Validator):
INPUT(_type='text', _name='name', requires=IS_DATE()) INPUT(_type='text', _name='name', requires=IS_DATE())
date has to be in the ISO8960 format YYYY-MM-DD date has to be in the ISO8960 format YYYY-MM-DD
timezome must be None or a pytz.timezone("America/Chicago") object
""" """
def __init__(self, format='%Y-%m-%d', def __init__(self, format='%Y-%m-%d',
error_message='Enter date as %(format)s', error_message='Enter date as %(format)s'):
timezone=None):
self.format = translate(format) self.format = translate(format)
self.error_message = str(error_message) self.error_message = str(error_message)
self.timezone = timezone
self.extremes = {} self.extremes = {}
def __call__(self, value): def __call__(self, value):
ovalue = value ovalue = value
if isinstance(value, datetime.date): if isinstance(value, datetime.date):
if self.timezone is not None:
value = value - datetime.timedelta(seconds=self.timezone*3600)
return (value, None) return (value, None)
try: try:
(y, m, d, hh, mm, ss, t0, t1, t2) = \ (y, m, d, hh, mm, ss, t0, t1, t2) = \
time.strptime(value, str(self.format)) time.strptime(value, str(self.format))
value = datetime.date(y, m, d) value = datetime.date(y, m, d)
if self.timezone is not None:
value = self.timezone.localize(value).astimezone(utc)
return (value, None) return (value, None)
except: except:
self.extremes.update(IS_DATETIME.nice(self.format)) self.extremes.update(IS_DATETIME.nice(self.format))
@@ -2200,11 +2246,7 @@ class IS_DATE(Validator):
format = format.replace('%Y', y) format = format.replace('%Y', y)
if year < 1900: if year < 1900:
year = 2000 year = 2000
if self.timezone is not None: d = datetime.date(year, value.month, value.day)
d = datetime.datetime(year, value.month, value.day)
d = d.replace(tzinfo=utc).astimezone(self.timezone)
else:
d = datetime.date(year, value.month, value.day)
return d.strftime(format) return d.strftime(format)
@@ -2255,7 +2297,8 @@ class IS_DATETIME(Validator):
time.strptime(value, str(self.format)) time.strptime(value, str(self.format))
value = datetime.datetime(y, m, d, hh, mm, ss) value = datetime.datetime(y, m, d, hh, mm, ss)
if self.timezone is not None: if self.timezone is not None:
value = self.timezone.localize(value).astimezone(utc) # TODO: https://github.com/web2py/web2py/issues/1094 (temporary solution)
value = self.timezone.localize(value).astimezone(utc).replace(tzinfo=None)
return (value, None) return (value, None)
except: except:
self.extremes.update(IS_DATETIME.nice(self.format)) self.extremes.update(IS_DATETIME.nice(self.format))
@@ -2304,8 +2347,7 @@ class IS_DATE_IN_RANGE(IS_DATE):
minimum=None, minimum=None,
maximum=None, maximum=None,
format='%Y-%m-%d', format='%Y-%m-%d',
error_message=None, error_message=None):
timezone=None):
self.minimum = minimum self.minimum = minimum
self.maximum = maximum self.maximum = maximum
if error_message is None: if error_message is None:
@@ -2317,8 +2359,7 @@ class IS_DATE_IN_RANGE(IS_DATE):
error_message = "Enter date in range %(min)s %(max)s" error_message = "Enter date in range %(min)s %(max)s"
IS_DATE.__init__(self, IS_DATE.__init__(self,
format=format, format=format,
error_message=error_message, error_message=error_message)
timezone=timezone)
self.extremes = dict(min=self.formatter(minimum), self.extremes = dict(min=self.formatter(minimum),
max=self.formatter(maximum)) max=self.formatter(maximum))
@@ -2844,9 +2885,11 @@ class CRYPT(object):
self.salt = salt self.salt = salt
def __call__(self, value): def __call__(self, value):
value = value and value[:self.max_length] v = value and str(value)[:self.max_length]
if len(value) < self.min_length: if not v or len(v) < self.min_length:
return ('', translate(self.error_message)) return ('', translate(self.error_message))
if isinstance(value, LazyCrypt):
return (value, None)
return (LazyCrypt(self, value), None) return (LazyCrypt(self, value), None)
# entropy calculator for IS_STRONG # entropy calculator for IS_STRONG
@@ -3374,7 +3417,8 @@ class IS_IPV4(Validator):
(number == self.localhost)): (number == self.localhost)):
ok = False ok = False
if not (self.is_private is None or self.is_private == if not (self.is_private is None or self.is_private ==
(sum([number[0] <= number <= number[1] for number in self.private]) > 0)): (sum([private_number[0] <= number <= private_number[1]
for private_number in self.private]) > 0)):
ok = False ok = False
if not (self.is_automatic is None or self.is_automatic == if not (self.is_automatic is None or self.is_automatic ==
(self.automatic[0] <= number <= self.automatic[1])): (self.automatic[0] <= number <= self.automatic[1])):
@@ -3479,7 +3523,7 @@ class IS_IPV6(Validator):
from gluon.contrib import ipaddr as ipaddress from gluon.contrib import ipaddr as ipaddress
try: try:
ip = ipaddress.IPv6Address(value) ip = ipaddress.IPv6Address(value.decode('utf-8'))
ok = True ok = True
except ipaddress.AddressValueError: except ipaddress.AddressValueError:
return (value, translate(self.error_message)) return (value, translate(self.error_message))
@@ -3491,7 +3535,7 @@ class IS_IPV6(Validator):
self.subnets = [self.subnets] self.subnets = [self.subnets]
for network in self.subnets: for network in self.subnets:
try: try:
ipnet = ipaddress.IPv6Network(network) ipnet = ipaddress.IPv6Network(network.decode('utf-8'))
except (ipaddress.NetmaskValueError, ipaddress.AddressValueError): except (ipaddress.NetmaskValueError, ipaddress.AddressValueError):
return (value, translate('invalid subnet provided')) return (value, translate('invalid subnet provided'))
if ip in ipnet: if ip in ipnet:
@@ -3700,20 +3744,22 @@ class IS_IPADDRESS(Validator):
def __call__(self, value): def __call__(self, value):
try: try:
import ipaddress from ipaddress import ip_address as IPAddress
from ipaddress import IPv6Address, IPv4Address
except ImportError: except ImportError:
from gluon.contrib import ipaddr as ipaddress from gluon.contrib.ipaddr import (IPAddress, IPv4Address,
IPv6Address)
try: try:
ip = ipaddress.IPAddress(value) ip = IPAddress(value.decode('utf-8'))
except ValueError, e: except ValueError:
return (value, translate(self.error_message)) return (value, translate(self.error_message))
if self.is_ipv4 and isinstance(ip, ipaddress.IPv6Address): if self.is_ipv4 and isinstance(ip, IPv6Address):
retval = (value, translate(self.error_message)) retval = (value, translate(self.error_message))
elif self.is_ipv6 and isinstance(ip, ipaddress.IPv4Address): elif self.is_ipv6 and isinstance(ip, IPv4Address):
retval = (value, translate(self.error_message)) retval = (value, translate(self.error_message))
elif self.is_ipv4 or isinstance(ip, ipaddress.IPv4Address): elif self.is_ipv4 or isinstance(ip, IPv4Address):
retval = IS_IPV4( retval = IS_IPV4(
minip=self.minip, minip=self.minip,
maxip=self.maxip, maxip=self.maxip,
@@ -3723,7 +3769,7 @@ class IS_IPADDRESS(Validator):
is_automatic=self.is_automatic, is_automatic=self.is_automatic,
error_message=self.error_message error_message=self.error_message
)(value) )(value)
elif self.is_ipv6 or isinstance(ip, ipaddress.IPv6Address): elif self.is_ipv6 or isinstance(ip, IPv6Address):
retval = IS_IPV6( retval = IS_IPV6(
is_private=self.is_private, is_private=self.is_private,
is_link_local=self.is_link_local, is_link_local=self.is_link_local,
+12 -7
View File
@@ -40,8 +40,8 @@ ProgramInfo = '''%s
%s %s
%s''' % (ProgramName, ProgramAuthor, ProgramVersion) %s''' % (ProgramName, ProgramAuthor, ProgramVersion)
if not sys.version[:3] in ['2.5', '2.6', '2.7']: if not sys.version[:3] in ['2.6', '2.7']:
msg = 'Warning: web2py requires Python 2.5, 2.6 or 2.7 but you are running:\n%s' msg = 'Warning: web2py requires Python 2.6 or 2.7 but you are running:\n%s'
msg = msg % sys.version msg = msg % sys.version
sys.stderr.write(msg) sys.stderr.write(msg)
@@ -56,8 +56,8 @@ def run_system_tests(options):
major_version = sys.version_info[0] major_version = sys.version_info[0]
minor_version = sys.version_info[1] minor_version = sys.version_info[1]
if major_version == 2: if major_version == 2:
if minor_version in (5, 6): if minor_version in (6,):
sys.stderr.write("Python 2.5 or 2.6\n") sys.stderr.write('Python 2.6\n')
ret = subprocess.call(['unit2', '-v', 'gluon.tests']) ret = subprocess.call(['unit2', '-v', 'gluon.tests'])
elif minor_version in (7,): elif minor_version in (7,):
call_args = [sys.executable, '-m', 'unittest', '-v', 'gluon.tests'] call_args = [sys.executable, '-m', 'unittest', '-v', 'gluon.tests']
@@ -1058,6 +1058,11 @@ def start_schedulers(options):
print 'starting single-scheduler for "%s"...' % app_ print 'starting single-scheduler for "%s"...' % app_
run(app_, True, True, None, False, code) run(app_, True, True, None, False, code)
return return
# Work around OS X problem: http://bugs.python.org/issue9405
import urllib
urllib.getproxies()
for app in apps: for app in apps:
app_, code = get_code_for_scheduler(app, options) app_, code = get_code_for_scheduler(app, options)
if not app_: if not app_:
@@ -1112,12 +1117,12 @@ def start(cron=True):
if hasattr(options, key): if hasattr(options, key):
setattr(options, key, getattr(options2, key)) setattr(options, key, getattr(options2, key))
logfile0 = os.path.join('extras', 'examples', 'logging.example.conf') logfile0 = os.path.join('examples', 'logging.example.conf')
if not os.path.exists('logging.conf') and os.path.exists(logfile0): if not os.path.exists('logging.conf') and os.path.exists(logfile0):
import shutil import shutil
sys.stdout.write("Copying logging.conf.example to logging.conf ... ") sys.stdout.write("Copying logging.conf.example to logging.conf ... ")
shutil.copyfile('logging.example.conf', logfile0) shutil.copyfile(logfile0, 'logging.conf')
sys.stdout.write("OK\n") sys.stdout.write('OK\n')
# ## if -T run doctests (no cron) # ## if -T run doctests (no cron)
if hasattr(options, 'test') and options.test: if hasattr(options, 'test') and options.test:
+1 -1
View File
@@ -195,7 +195,7 @@ NameVirtualHost *:80
NameVirtualHost *:443 NameVirtualHost *:443
<VirtualHost *:80> <VirtualHost *:80>
WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1 WSGIDaemonProcess web2py user=apache group=apache
WSGIProcessGroup web2py WSGIProcessGroup web2py
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
WSGIPassAuthorization On WSGIPassAuthorization On
+1 -1
View File
@@ -299,7 +299,7 @@ NameVirtualHost *:80
NameVirtualHost *:443 NameVirtualHost *:443
<VirtualHost *:80> <VirtualHost *:80>
WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1 WSGIDaemonProcess web2py user=apache group=apache
WSGIProcessGroup web2py WSGIProcessGroup web2py
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
+3 -2
View File
@@ -1,3 +1,4 @@
#!/bin/bash
echo "This script will: echo "This script will:
1) Install modules needed to run web2py on Fedora and CentOS/RHEL 1) Install modules needed to run web2py on Fedora and CentOS/RHEL
2) Install Python 2.6 to /opt and recompile wsgi if not provided 2) Install Python 2.6 to /opt and recompile wsgi if not provided
@@ -27,7 +28,7 @@ Press ENTER to continue...[ctrl+C to abort]"
read CONFIRM read CONFIRM
#!/bin/bash
### ###
### Phase 0 - This may get messy. Lets work from a temporary directory ### Phase 0 - This may get messy. Lets work from a temporary directory
@@ -301,7 +302,7 @@ NameVirtualHost *:80
NameVirtualHost *:443 NameVirtualHost *:443
<VirtualHost *:80> <VirtualHost *:80>
WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1 WSGIDaemonProcess web2py user=apache group=apache
WSGIProcessGroup web2py WSGIProcessGroup web2py
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
WSGIPassAuthorization On WSGIPassAuthorization On
+235
View File
@@ -0,0 +1,235 @@
#!/bin/bash
# This script will install web2py with nginx+uwsgi on centos 7
# This script is based on excellent tutorial by Justin Ellingwood on
# https://www.digitalocean.com/community/tutorials/how-to-deploy-web2py-python-applications-with-uwsgi-and-nginx-on-centos-7
#
# Phase 1: First, let's ask a few things
#
read -p "Enter username under which web2py will be installed [web2py]: " USERNAME
USERNAME=${USERNAME:-web2py}
read -p "Enter path where web2py will be installed [/opt/web2py_apps]: " WEB2PY_PATH
WEB2PY_PATH=${WEB2PY_PATH:-/opt/web2py_apps}
read -p "Web2py subdirectory will be called: [web2py]: " WEB2PY_APP
WEB2PY_APP=${WEB2PY_APP:-web2py}
read -p "Enter your web2py admin password: " WEB2PY_PASS
read -p "Enter your domain name: " YOUR_SERVER_DOMAIN
# open new user
useradd -d $WEB2PY_PATH $USERNAME
# if it's not already open, let's create a directory for web2py
mkdir -p $WEB2PY_PATH
# now let's create a self signed certificate
cd $WEB2PY_PATH
openssl req -x509 -new -newkey rsa:4096 -days 3652 -nodes -keyout $WEB2PY_APP.key -out $WEB2PY_APP.crt
#
# phase 2: That was all the input that we needed so let's install the components
#
echo "Installing necessary components"
# Verify packages are up to date
yum -y upgrade
# Install required packages
yum install -y epel-release
yum install -y python-devel python-pip gcc nginx wget unzip python-psycopg2 MySQL-python
# download and unzip web2py
echo "Downloading web2py"
cd $WEB2PY_PATH
wget http://web2py.com/examples/static/web2py_src.zip
unzip web2py_src.zip
rm web2py_src.zip
# preparing wsgihandler
chown -R $USERNAME.$USERNAME $WEB2PY_PATH/$WEB2PY_APP
mv $WEB2PY_PATH/$WEB2PY_APP/handlers/wsgihandler.py $WEB2PY_PATH/$WEB2PY_APP
# now let's install uwsgi
pip install uwsgi
# preparing directories
mkdir -p /etc/uwsgi/sites
mkdir -p /var/log/uwsgi
mkdir -p /etc/nginx/ssl/
#
# Phase 3: Ok, everything is installed now so we'll configure things
#
# Create configuration file for uwsgi in /etc/uwsgi/$WEB2PY_APP.ini
echo '[uwsgi]
chdir = WEB2PY_PATH_PLACEHOLDER/WEB2PY_APP_PLACEHOLDER
module = wsgihandler:application
master = true
processes = 5
uid = USERNAME_PLACEHOLDER
socket = /run/uwsgi/WEB2PY_APP_PLACEHOLDER.sock
chown-socket = USERNAME_PLACEHOLDER:nginx
chmod-socket = 660
vacuum = true
' >/etc/uwsgi/sites/$WEB2PY_APP.ini
sed -i "s@WEB2PY_PATH_PLACEHOLDER@$WEB2PY_PATH@" /etc/uwsgi/sites/$WEB2PY_APP.ini
sed -i "s@WEB2PY_APP_PLACEHOLDER@$WEB2PY_APP@" /etc/uwsgi/sites/$WEB2PY_APP.ini
sed -i "s@USERNAME_PLACEHOLDER@$USERNAME@" /etc/uwsgi/sites/$WEB2PY_APP.ini
# Create a daemon configuration file for uwsgi
cat > /etc/systemd/system/uwsgi.service <<EOF
[Unit]
Description=uWSGI Emperor service
[Service]
ExecStartPre=/usr/bin/bash -c 'mkdir -p /run/uwsgi; chown USERNAME_PLACEHOLDER:nginx /run/uwsgi'
ExecStart=/usr/bin/uwsgi --emperor /etc/uwsgi/sites
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all
[Install]
WantedBy=multi-user.target
EOF
sed -i "s@USERNAME_PLACEHOLDER@$USERNAME@" /etc/systemd/system/uwsgi.service
#chmod 777 /etc/systemd/system/uwsgi.service
# create a nginx configuration file
cat > /etc/nginx/nginx.conf <<EOF
# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
'\$status \$body_bytes_sent "\$http_referer" '
'"\$http_user_agent" "\$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name YOUR_SERVER_DOMAIN_PLACEHOLDER;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location ~* /(\w+)/static/ {
root WEB2PY_PATH_PLACEHOLDER/WEB2PY_APP_PLACEHOLDER/applications/;
}
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/WEB2PY_APP_PLACEHOLDER.sock;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
server {
listen 443;
server_name YOUR_SERVER_DOMAIN_PLACEHOLDER;
ssl on;
ssl_certificate /etc/nginx/ssl/WEB2PY_APP_PLACEHOLDER.crt;
ssl_certificate_key /etc/nginx/ssl/WEB2PY_APP_PLACEHOLDER.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
ssl_prefer_server_ciphers on;
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/WEB2PY_APP_PLACEHOLDER.sock;
}
}
}
EOF
sed -i "s@YOUR_SERVER_DOMAIN_PLACEHOLDER@$YOUR_SERVER_DOMAIN@" /etc/nginx/nginx.conf
sed -i "s@WEB2PY_PATH_PLACEHOLDER@$WEB2PY_PATH@" /etc/nginx/nginx.conf
sed -i "s@WEB2PY_APP_PLACEHOLDER@$WEB2PY_APP@" /etc/nginx/nginx.conf
#
# Phase 4: everything is configured now, just a few final touches
#
# copying certificates to nginx directory
mv $WEB2PY_PATH/$WEB2PY_APP.crt* /etc/nginx/ssl
mv $WEB2PY_PATH/$WEB2PY_APP.key* /etc/nginx/ssl
# creating web2py admin password
cd $WEB2PY_PATH/$WEB2PY_APP
python -c "from gluon.main import save_password; save_password('$WEB2PY_PASS',443)"
chown -R $USERNAME.$USERNAME $WEB2PY_PATH/$WEB2PY_APP
# taking care of permissions
chmod 700 /etc/nginx/ssl
usermod -a -G $USERNAME nginx
chmod 710 $WEB2PY_PATH
# enabling daemons
systemctl start nginx
systemctl start uwsgi
systemctl enable nginx
systemctl enable uwsgi
# If firewall is active make sure these ports are open
firewall-cmd --zone=public --add-port=80/tcp --permanent
firewall-cmd --zone=public --add-port=443/tcp --permanent
firewall-cmd --zone=public --add-port=22/tcp --permanent
firewall-cmd --reload
echo
echo 'Web2py is now installed on this server!'
echo
+1 -1
View File
@@ -84,7 +84,7 @@ server {
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m; ssl_session_timeout 10m;
ssl_ciphers ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA; ssl_ciphers ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA;
ssl_protocols SSLv3 TLSv1; ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
keepalive_timeout 70; keepalive_timeout 70;
location / { location / {
#uwsgi_pass 127.0.0.1:9001; #uwsgi_pass 127.0.0.1:9001;
+25 -6
View File
@@ -1,7 +1,8 @@
#!/bin/bash
echo "This script will: echo "This script will:
1) install all modules need to run web2py on Ubuntu 14.04 1) install all modules need to run web2py on Ubuntu 14.04
2) install web2py in /home/www-data/ 2) install web2py in /home/www-data/
3) create a self signed sll certificate 3) create a self signed ssl certificate
4) setup web2py with mod_wsgi 4) setup web2py with mod_wsgi
5) overwrite /etc/apache2/sites-available/default 5) overwrite /etc/apache2/sites-available/default
6) restart apache. 6) restart apache.
@@ -12,7 +13,7 @@ Press a key to continue...[ctrl+C to abort]"
read CONFIRM read CONFIRM
#!/bin/bash
# optional # optional
# dpkg-reconfigure console-setup # dpkg-reconfigure console-setup
# dpkg-reconfigure timezoneconf # dpkg-reconfigure timezoneconf
@@ -84,13 +85,31 @@ openssl x509 -noout -fingerprint -text < /etc/apache2/ssl/self_signed.cert > /et
echo "rewriting your apache config file to use mod_wsgi" echo "rewriting your apache config file to use mod_wsgi"
echo "=================================================" echo "================================================="
echo ' echo '
WSGIDaemonProcess web2py user=www-data group=www-data processes=1 threads=1 WSGIDaemonProcess web2py user=www-data group=www-data
<VirtualHost *:80> <VirtualHost *:80>
RewriteEngine On WSGIProcessGroup web2py
RewriteCond %{HTTPS} !=on WSGIScriptAlias / /home/www-data/web2py/wsgihandler.py
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L] WSGIPassAuthorization On
<Directory /home/www-data/web2py>
AllowOverride None
Require all denied
<Files wsgihandler.py>
Require all granted
</Files>
</Directory>
AliasMatch ^/([^/]+)/static/(?:_[\d]+.[\d]+.[\d]+/)?(.*) \
/home/www-data/web2py/applications/$1/static/$2
<Directory /home/www-data/web2py/applications/*/static/>
Options -Indexes
ExpiresActive On
ExpiresDefault "access plus 1 hour"
Require all granted
</Directory>
CustomLog /var/log/apache2/access.log common CustomLog /var/log/apache2/access.log common
ErrorLog /var/log/apache2/error.log ErrorLog /var/log/apache2/error.log
+169
View File
@@ -0,0 +1,169 @@
"This script will work fine for a few cases 'by default':"
" - completely CLEAN WS2012R2 host"
" - python 2.7 installed in the default path"
" - wfasctgi installed on the default path"
"It'll install web2py under the default website "
" You can use it as a boilerplate to automate your deployments"
" but it still is released AS IT IS. "
"BIG FAT WARNING: It will install a bunch of dependecies
Inspect the source before executing it"
""
""
$ErrorActionPreference = 'stop'
$REALLY_SURE = Read-Host "Do you want to start with web2py deployment? [y/N]"
if (!@('y', 'Y') -contains $REALLY_SURE) {
"Ok, Exiting without doing anything"
exit 1
}
#setting root folder
$rootfolder = $pwd
### utilities - start
function ask_a_question($question) {
$response = Read-Host "$question [Y/n]"
if (@('Y', 'y', '', $null) -contains $response) {
return $true
} else {
return $false
}
}
function unzip_me {
#Load the assembly
[System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null
#Unzip the file
[System.IO.Compression.ZipFile]::ExtractToDirectory($pathToZip, $targetDir)
}
### utilities - end
#install 4.5 that is needed for a bunch of things anyway
Install-WindowsFeature Net-Framework-45-Core
#fetch web2py
$web2py_url = 'http://www.web2py.com/examples/static/web2py_src.zip'
$web2py_file = "$pwd\web2py_src.zip"
if (!(Test-Path $web2py_file)) {
(new-object net.webclient).DownloadFile($web2py_url, $web2py_file)
}
#Load the assembly
[System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null
#Unzip the file
[System.IO.Compression.ZipFile]::ExtractToDirectory($web2py_file, $pwd)
#features installation (IIS, needed modules, python, chocolatey, etc)
$installfeatures = ask_a_question('Do you want to install needed features?')
if ($installfeatures) {
Install-WindowsFeature Web-Server,Web-Default-Doc,Web-Static-Content,Web-Http-Redirect,Web-Http-Logging,Web-Request-Monitor,`
Web-Http-Tracing,Web-Stat-Compression,Web-Dyn-Compression,Web-Filtering,Web-Basic-Auth,Web-Windows-Auth,Web-AppInit,`
Web-CGI,Web-WebSockets,Web-Mgmt-Console,Web-Net-Ext45
}
$copy_web2py = ask_a_question("Copy web2py to the default website root?")
if ($copy_web2py) {
Import-Module WebAdministration
$available_websites = Get-Website
if ($available_websites[0] -eq $null) {
$default_one = $available_websites
} else {
$default_one = $available_websites[0]
}
$iis_root = [System.Environment]::ExpandEnvironmentVariables($default_one.PhysicalPath)
Copy-Item "$rootfolder\web2py\*" $iis_root -Recurse
$rootfolder = $iis_root
$acl = (Get-Item $rootfolder).GetAccessControl('Access')
$identity = "BUILTIN\IIS_IUSRS"
$fileSystemRights = "Modify"
$inheritanceFlags = "ContainerInherit, ObjectInherit"
$propagationFlags = "None"
$accessControlType = "Allow"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, $fileSystemRights, $inheritanceFlags, $propagationFlags, $accessControlType)
$acl.SetAccessRule($rule)
Set-Acl $rootfolder $acl
}
$create_cert = ask_a_question("Do you want to create a self-signed SSL cert?")
if ($create_cert) {
$cert = New-SelfSignedCertificate -DnsName ("localtest.me","*.localtest.me") -CertStoreLocation cert:\LocalMachine\My
$rootStore = Get-Item cert:\LocalMachine\Root
$rootStore.Open("ReadWrite")
$rootStore.Add($cert)
$rootStore.Close();
Import-Module WebAdministration
Set-Location IIS:\SslBindings
New-WebBinding -Name "Default Web Site" -IP "*" -Port 443 -Protocol https
$cert | New-Item 0.0.0.0!443
Set-Location $pwd
}
"checking for chocolatey"
if (Get-Command "choco.exe" -ErrorAction SilentlyContinue)
{
"chocolatey found"
} else {
"installing chocolatey"
(new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1') | iex
}
"installing url-rewrite"
choco install UrlRewrite
$pythonexe = Read-Host 'Python.exe path [C:\Python27\python.exe]'
if (($pythonexe -eq '') -or ($pythonexe -eq $null)) {
$pythonexe = 'C:\Python27\python.exe'
}
if (!(Test-Path $pythonexe)) {
"ERROR: python executable not found"
$pythonwanted = ask_a_question("do you want to install it automatically?")
if ($pythonwanted) {
choco install webpicmd
WebpiCmd.exe /Install /Products:WFastCgi_21_279
$pythonexe = 'C:\Python27\python.exe'
}
else {
exit 1
}
}
$wfastcgipath = Read-Host 'wfastcgi.py path [C:\Python27\Scripts\wfastcgi.py]'
if (($wfastcgipath -eq '') -or ($wfastcgipath -eq $null)) {
$wfastcgipath = 'C:\Python27\Scripts\wfastcgi.py'
}
if (-not (Test-Path $wfastcgipath)) {
"ERROR: wfastcgi.py not found"
$wfastcgiwanted = ask_a_question("do you want to install it automatically?")
if ($wfastcgiwanted) {
choco install webpicmd
WebpiCmd.exe /Install /Products:WFastCgi_21_279
} else {
exit 1
}
}
$pythondir = Split-Path c:\python27\python.exe
#installing dependencies
$env:Path = $env:Path + ";$pythondir;$pythondir\Scripts"
pip install pypiwin32
$PW = Read-Host 'Web2py Admin Password'
$appcmdpath = "$env:windir\system32\inetsrv\appcmd.exe"
& $appcmdpath set config /section:system.webServer/fastCGI "/+[fullPath='$pythonexe', arguments='$wfastcgipath']"
& $appcmdpath unlock config -section:system.webServer/handlers
& cd $rootfolder
& $pythonexe -c "from gluon.main import save_password; save_password('$PW',443)"
$webconfig_template = Join-Path $rootfolder "examples\web.config"
$destination = Join-Path $rootfolder "web.config"
$scriptprocessor = 'scriptProcessor="{0}|{1}"' -f $pythonexe, $wfastcgipath
(Get-Content $webconfig_template) | Foreach-Object {$_ -replace 'scriptProcessor="SCRIPT_PROCESSOR"', $scriptprocessor} | where {$_ -ne ""} | Set-Content $destination
""
"Installation finished. Web2py is available either on http://localhost/ or at https://localtest.me/"
""
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Post error tickets to slack on a 5 minute schedule.
#
# Proper use depends on having created a web-hook through Slack, and having set
# that value in your app's model as the value of global_settings.slack_hook.
# Details on creating web-hooks can be found at https://slack.com/integrations
#
# requires the Requests module for posting to slack, other requirements are
# standard or provided by web2py
#
# Usage (on Unices), replace myapp with the name of your application and run:
# nohup python web2py.py -S myapp -M -R scripts/tickets2slack.py &
import sys
import os
import time
import pickle
import json
try:
import requests
except ImportError as e:
print "missing module 'Requests', aborting."
sys.exit(1)
from gluon import URL
from gluon.utils import md5_hash
from gluon.restricted import RestrictedError
from gluon.settings import global_settings
path = os.path.join(request.folder, 'errors')
sent_errors_file = os.path.join(path, 'slack_errors.pickle')
hashes = {}
if os.path.exists(sent_errors_file):
try:
with open(sent_errors_file, 'rb') as f:
hashes = pickle.load(f)
except Exception as _:
pass
# ## CONFIGURE HERE
SLEEP_MINUTES = 5
ALLOW_DUPLICATES = False
global_settings.slack_hook = global_settings.slack_hook or \
'https://hooks.slack.com/services/your_service'
# ## END CONFIGURATION
while 1:
for file_name in os.listdir(path):
if file_name == 'slack_errors.pickle':
continue
if not ALLOW_DUPLICATES:
key = md5_hash(file_name)
if key in hashes:
continue
hashes[key] = 1
error = RestrictedError()
try:
error.load(request, request.application, file_name)
except Exception as _:
continue # not an exception file?
url = URL(a='admin', f='ticket', args=[request.application, file],
scheme=True)
payload = json.dumps(dict(text="Error in %(app)s.\n%(url)s" %
dict(app=request.application, url=url)))
requests.post(global_settings.slack_hook, data=dict(payload=payload))
with open(sent_errors_file, 'wb') as f:
pickle.dump(hashes, f)
time.sleep(SLEEP_MINUTES * 60)
+21
View File
@@ -0,0 +1,21 @@
description "web2py task scheduler"
# INSTRUCTIONS:
# COPY THIS FILE IN:
# /etc/init/web2py-scheduler.con
#
# To start/stop the scheduler, use
# "sudo start web2py-scheduler"
# "sudo stop web2py-scheduler"
# "sudo status web2py-scheduler"
#
# YOU MAY HAVE TO EDIT PATH TO WEB2PY BELOW
start on (local-filesystems and net-device-up IFACE=eth0)
stop on shutdown
# Give up if restart occurs 8 times in 60 seconds.
respawn limit 8 60
exec sudo -u www-data python /home/www-data/web2py/web2py.py -K parking > /tmp/scheduler.out
respawn
+1 -1
View File
@@ -62,7 +62,7 @@ do_start()
start-stop-daemon --stop --test --quiet --pidfile $PIDFILE \ start-stop-daemon --stop --test --quiet --pidfile $PIDFILE \
&& return 1 && return 1
start-stop-daemon --start --quiet --pidfile $PIDFILE \ start-stop-daemon --start --quiet -m --pidfile $PIDFILE \
${DAEMON_USER:+--chuid $DAEMON_USER} --chdir $DAEMON_DIR \ ${DAEMON_USER:+--chuid $DAEMON_USER} --chdir $DAEMON_DIR \
--background --exec $DAEMON -- $DAEMON_ARGS \ --background --exec $DAEMON -- $DAEMON_ARGS \
|| return 2 || return 2
+6 -7
View File
@@ -3,6 +3,8 @@
import os import os
import sys import sys
from multiprocessing import freeze_support
# import gluon.import_all ##### This should be uncommented for py2exe.py
if hasattr(sys, 'frozen'): if hasattr(sys, 'frozen'):
path = os.path.dirname(os.path.abspath(sys.executable)) # for py2exe path = os.path.dirname(os.path.abspath(sys.executable)) # for py2exe
@@ -14,17 +16,14 @@ os.chdir(path)
sys.path = [path] + [p for p in sys.path if not p == path] sys.path = [path] + [p for p in sys.path if not p == path]
# import gluon.import_all ##### This should be uncommented for py2exe.py # important that this import is after the os.chdir
import gluon.widget import gluon.widget
# Start Web2py and Web2py cron service! # Start Web2py and Web2py cron service!
if __name__ == '__main__': if __name__ == '__main__':
try: freeze_support()
from multiprocessing import freeze_support if 'COVERAGE_PROCESS_START' in os.environ:
freeze_support()
except:
sys.stderr.write('Sorry, -K only supported for python 2.6-2.7\n')
if os.environ.has_key("COVERAGE_PROCESS_START"):
try: try:
import coverage import coverage
coverage.process_startup() coverage.process_startup()