Compare commits

...
172 Commits
Author SHA1 Message Date
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
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
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
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
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
62 changed files with 1315 additions and 360 deletions
+1
View File
@@ -58,3 +58,4 @@ HOWTO-web2py-devel
*.sublime-project *.sublime-project
*.sublime-workspace *.sublime-workspace
.idea/* .idea/*
site-packages/
+2 -2
View File
@@ -17,14 +17,14 @@ 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
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
+22
View File
@@ -1,3 +1,25 @@
## 2.12.1
- 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
+1 -1
View File
@@ -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.4-beta+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION echo 'Version 2.12.2-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
+5 -4
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
@@ -63,7 +64,7 @@ That's it!!!
packages/ > web2py submodules packages/ > web2py submodules
dal/ dal/
contrib/ > third party libraries contrib/ > third party libraries
tests/ > unittests tests/ > unittests
applications/ > are the apps applications/ > are the apps
admin/ > web based IDE admin/ > web based IDE
... ...
+1 -1
View File
@@ -1 +1 @@
Version 2.10.4-stable+timestamp.2015.04.26.09.05.21 Version 2.12.2-stable+timestamp.2015.08.09.09.27.09
+10 -12
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:
@@ -420,7 +420,7 @@ def ccache():
'oldest': time.time(), 'oldest': time.time(),
'keys': [] 'keys': []
} }
disk = copy.copy(ram) disk = copy.copy(ram)
total = copy.copy(ram) total = copy.copy(ram)
disk['keys'] = [] disk['keys'] = []
@@ -480,12 +480,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'])
@@ -577,9 +577,7 @@ def bg_graph_model():
group = meta_graphmodel['group'].replace(' ', '') group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group): if not subgraphs.has_key(group):
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})
+51 -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'))]
@@ -744,7 +772,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 +782,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 +895,9 @@ def resolve():
def getclass(item): def getclass(item):
""" Determine item class """ """ Determine item class """
operators = {' ':'normal', '+':'plus', '-':'minus'}
if item[0] == ' ':
return 'normal' return operators[item[0]]
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 +1092,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 +1230,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 +1534,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)
File diff suppressed because one or more lines are too long
+23 -21
View File
@@ -490,12 +490,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 +515,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');
@@ -586,12 +586,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 +636,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 +648,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 +657,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);
}); });
@@ -730,4 +732,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers;
web2py_trap_link = jQuery.web2py.trap_link; web2py_trap_link = jQuery.web2py.trap_link;
web2py_calc_entropy = jQuery.web2py.calc_entropy; web2py_calc_entropy = jQuery.web2py.calc_entropy;
*/ */
/* compatibility code - end*/ /* compatibility code - end*/
+3 -1
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):
@@ -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>
+10 -12
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:
@@ -420,7 +420,7 @@ def ccache():
'oldest': time.time(), 'oldest': time.time(),
'keys': [] 'keys': []
} }
disk = copy.copy(ram) disk = copy.copy(ram)
total = copy.copy(ram) total = copy.copy(ram)
disk['keys'] = [] disk['keys'] = []
@@ -480,12 +480,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'])
@@ -577,9 +577,7 @@ def bg_graph_model():
group = meta_graphmodel['group'].replace(' ', '') group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group): if not subgraphs.has_key(group):
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,7 @@
#### 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]]
- [[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 +23,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]]
File diff suppressed because one or more lines are too long
+23 -21
View File
@@ -490,12 +490,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 +515,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');
@@ -586,12 +586,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 +636,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 +648,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 +657,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);
}); });
@@ -730,4 +732,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers;
web2py_trap_link = jQuery.web2py.trap_link; web2py_trap_link = jQuery.web2py.trap_link;
web2py_calc_entropy = jQuery.web2py.calc_entropy; web2py_calc_entropy = jQuery.web2py.calc_entropy;
*/ */
/* compatibility code - end*/ /* compatibility code - end*/
@@ -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>
+10 -12
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:
@@ -420,7 +420,7 @@ def ccache():
'oldest': time.time(), 'oldest': time.time(),
'keys': [] 'keys': []
} }
disk = copy.copy(ram) disk = copy.copy(ram)
total = copy.copy(ram) total = copy.copy(ram)
disk['keys'] = [] disk['keys'] = []
@@ -480,12 +480,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'])
@@ -577,9 +577,7 @@ def bg_graph_model():
group = meta_graphmodel['group'].replace(' ', '') group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group): if not subgraphs.has_key(group):
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))
@@ -30,6 +30,7 @@ def user():
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/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)
+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
@@ -24,7 +24,9 @@ div.flash.alert:hover {
.ie-lte8 div.flash:hover { .ie-lte8 div.flash:hover {
filter: alpha(opacity=25); filter: alpha(opacity=25);
} }
.main-container {
margin-top: 20px;
}
div.error { div.error {
width: auto; width: auto;
@@ -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%;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8 -6
View File
@@ -586,12 +586,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') {
+1 -1
View File
@@ -75,7 +75,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
+4 -3
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
@@ -118,10 +120,9 @@ def app_cleanup(app, request):
r = False r = False
# 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))
+7 -2
View File
@@ -473,9 +473,14 @@ 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})
self.storage.release(CacheAbstract.cache_stats_name) self.storage.release(CacheAbstract.cache_stats_name)
+9 -8
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:
@@ -532,10 +532,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 +578,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 +632,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,8 +650,8 @@ 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 = response.view
folder = request.folder folder = request.folder
path = pjoin(folder, 'compiled') path = pjoin(folder, 'compiled')
+10
View File
@@ -33,6 +33,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 +81,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
@@ -610,6 +618,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,
+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):
+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'
+1 -1
View File
@@ -12,7 +12,7 @@ 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
+48 -32
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,6 +203,7 @@ 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
@@ -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)
-1
View File
@@ -376,7 +376,6 @@ def wsgibase(environ, responder):
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']
# ################################################## # ##################################################
+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):
""" """
+1 -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)
+73 -58
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:
@@ -859,14 +879,14 @@ def formstyle_bootstrap3_stacked(form, fields):
label = '' label = ''
elif isinstance(controls, (SELECT, TEXTAREA)): elif isinstance(controls, (SELECT, TEXTAREA)):
controls.add_class('form-control') controls.add_class('form-control')
elif isinstance(controls, SPAN): elif isinstance(controls, SPAN):
_controls = P(controls.components) _controls = P(controls.components)
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')
if isinstance(label, LABEL): if isinstance(label, LABEL):
label['_class'] = 'control-label' label['_class'] = 'control-label'
@@ -909,9 +929,9 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3):
label = '' label = ''
elif isinstance(controls, (SELECT, TEXTAREA)): elif isinstance(controls, (SELECT, TEXTAREA)):
controls.add_class('form-control') controls.add_class('form-control')
elif isinstance(controls, SPAN): elif isinstance(controls, SPAN):
_controls = P(controls.components, _controls = P(controls.components,
_class="form-control-static %s" % col_class) _class="form-control-static %s" % col_class)
elif isinstance(controls, UL): elif isinstance(controls, UL):
for e in controls.elements("input"): for e in controls.elements("input"):
@@ -1126,7 +1146,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
@@ -1678,7 +1699,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:
@@ -1691,6 +1712,7 @@ class SQLFORM(FORM):
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 +1777,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 +1800,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 +1862,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 +1884,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 +1996,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
@@ -2069,18 +2099,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 +2135,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 +2567,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 +2589,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 +2599,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 +2618,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 +2689,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 +2702,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()
+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
+16 -1
View File
@@ -4,6 +4,7 @@
Unit tests for gluon.dal Unit tests for gluon.dal
""" """
import os
import unittest import unittest
from fix_path import fix_sys_path from fix_path import fix_sys_path
@@ -12,8 +13,15 @@ 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 +30,26 @@ 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
""" """
if __name__ == '__main__':
unittest.main()
tearDownModule()
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Unit tests for gluon.globals
"""
import unittest
from fix_path import fix_sys_path
fix_sys_path(__file__)
from gluon.globals import Response
from gluon import URL
class testResponse(unittest.TestCase):
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, '')
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__':
+386 -36
View File
@@ -60,7 +60,7 @@ except ImportError:
# fallback to pure-Python module # fallback to pure-Python module
import gluon.contrib.simplejson as json_parser import gluon.contrib.simplejson as json_parser
__all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'Wiki', __all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki',
'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate']
### mind there are two loggers here (logger and crud.settings.logger)! ### mind there are two loggers here (logger and crud.settings.logger)!
@@ -765,10 +765,13 @@ class Mail(object):
result = {} result = {}
try: try:
if self.settings.server == 'logging': if self.settings.server == 'logging':
logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % entry = 'email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \
('-' * 40, sender, ('-' * 40, sender, ', '.join(to), subject, text or html, '-' * 40)
', '.join(to), subject, logger.warn(entry)
text or html, '-' * 40)) elif self.settings.server.startswith('logging:'):
entry = 'email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \
('-' * 40, sender, ', '.join(to), subject, text or html, '-' * 40)
open(self.settings.server[8:], 'a').write(entry)
elif self.settings.server == 'gae': elif self.settings.server == 'gae':
xcc = dict() xcc = dict()
if cc: if cc:
@@ -779,23 +782,23 @@ class Mail(object):
xcc['reply_to'] = reply_to xcc['reply_to'] = reply_to
from google.appengine.api import mail from google.appengine.api import mail
attachments = attachments and [mail.Attachment( attachments = attachments and [mail.Attachment(
a.my_filename, a.my_filename,
a.my_payload, a.my_payload,
contebt_id='<attachment-%s>' % k contebt_id='<attachment-%s>' % k
) for k,a in enumerate(attachments) if not raw] ) for k,a in enumerate(attachments) if not raw]
if attachments: if attachments:
result = mail.send_mail( result = mail.send_mail(
sender=sender, to=origTo, sender=sender, to=origTo,
subject=subject, body=text, html=html, subject=unicode(subject), body=unicode(text), html=html,
attachments=attachments, **xcc) attachments=attachments, **xcc)
elif html and (not raw): elif html and (not raw):
result = mail.send_mail( result = mail.send_mail(
sender=sender, to=origTo, sender=sender, to=origTo,
subject=subject, body=text, html=html, **xcc) subject=unicode(subject), body=unicode(text), html=html, **xcc)
else: else:
result = mail.send_mail( result = mail.send_mail(
sender=sender, to=origTo, sender=sender, to=origTo,
subject=subject, body=text, **xcc) subject=unicode(subject), body=unicode(text), **xcc)
else: else:
smtp_args = self.settings.server.split(':') smtp_args = self.settings.server.split(':')
kwargs = dict(timeout=self.settings.timeout) kwargs = dict(timeout=self.settings.timeout)
@@ -965,7 +968,142 @@ class Recaptcha(DIV):
return XML(captcha).xml() return XML(captcha).xml()
# this should only be used for catcha and perhaps not even for that class Recaptcha2(DIV):
"""
Experimental:
Creates a DIV holding the newer Recaptcha from Google (v2)
Args:
request : the request. If not passed, uses current request
public_key : the public key Google gave you
private_key : the private key Google gave you
error_message : the error message to show if verification fails
label : the label to use
options (dict) : takes these parameters
- hl
- theme
- type
- tabindex
- callback
- expired-callback
see https://developers.google.com/recaptcha/docs/display for docs about those
comment : the comment
Examples:
Use as::
form = FORM(Recaptcha2(public_key='...',private_key='...'))
or::
form = SQLFORM(...)
form.append(Recaptcha2(public_key='...',private_key='...'))
to protect the login page instead, use::
from gluon.tools import Recaptcha2
auth.settings.captcha = Recaptcha2(request, public_key='...',private_key='...')
"""
API_URI = 'https://www.google.com/recaptcha/api.js'
VERIFY_SERVER = 'https://www.google.com/recaptcha/api/siteverify'
def __init__(self,
request=None,
public_key='',
private_key='',
error_message='invalid',
label='Verify:',
options=None,
comment='',
):
request = request or current.request
self.request_vars = request and request.vars or current.request.vars
self.remote_addr = request.env.remote_addr
self.public_key = public_key
self.private_key = private_key
self.errors = Storage()
self.error_message = error_message
self.components = []
self.attributes = {}
self.label = label
self.options = options or {}
self.comment = comment
def _validate(self):
recaptcha_response_field = self.request_vars.pop('g-recaptcha-response', None)
remoteip = self.remote_addr
if not recaptcha_response_field:
self.errors['captcha'] = self.error_message
return False
params = urllib.urlencode({
'secret': self.private_key,
'remoteip': remoteip,
'response': recaptcha_response_field,
})
request = urllib2.Request(
url=self.VERIFY_SERVER,
data=params,
headers={'Content-type': 'application/x-www-form-urlencoded',
'User-agent': 'reCAPTCHA Python'})
httpresp = urllib2.urlopen(request)
content = httpresp.read()
httpresp.close()
try:
response_dict = json_parser.loads(content)
except:
self.errors['captcha'] = self.error_message
return False
if response_dict.get('success', False):
self.request_vars.captcha = ''
return True
else:
self.errors['captcha'] = self.error_message
return False
def xml(self):
api_uri = self.API_URI
hl = self.options.pop('hl', None)
if hl:
api_uri = self.API_URI + '?hl=%s' % hl
public_key = self.public_key
self.options['sitekey'] = public_key
captcha = DIV(
SCRIPT(_src=api_uri, _async='', _defer=''),
DIV(_class="g-recaptcha", data=self.options),
TAG.noscript(XML("""
<div style="width: 302px; height: 352px;">
<div style="width: 302px; height: 352px; position: relative;">
<div style="width: 302px; height: 352px; position: absolute;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=%(public_key)s"
frameborder="0" scrolling="no"
style="width: 302px; height:352px; border-style: none;">
</iframe>
</div>
<div style="width: 250px; height: 80px; position: absolute; border-style: none;
bottom: 21px; left: 25px; margin: 0px; padding: 0px; right: 25px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response"
class="g-recaptcha-response"
style="width: 250px; height: 80px; border: 1px solid #c1c1c1;
margin: 0px; padding: 0px; resize: none;" value="">
</textarea>
</div>
</div>
</div>""" % dict(public_key=public_key))
)
)
if not self.errors.captcha:
return XML(captcha).xml()
else:
captcha.append(DIV(self.errors['captcha'], _class='error'))
return XML(captcha).xml()
# this should only be used for captcha and perhaps not even for that
def addrow(form, a, b, c, style, _id, position=-1): def addrow(form, a, b, c, style, _id, position=-1):
if style == "divs": if style == "divs":
form[0].insert(position, DIV(DIV(LABEL(a), _class='w2p_fl'), form[0].insert(position, DIV(DIV(LABEL(a), _class='w2p_fl'),
@@ -987,6 +1125,15 @@ def addrow(form, a, b, c, style, _id, position=-1):
DIV(b, SPAN(c, _class='inline-help'), DIV(b, SPAN(c, _class='inline-help'),
_class='controls'), _class='controls'),
_class='control-group', _id=_id)) _class='control-group', _id=_id))
elif style == "bootstrap3_inline":
form[0].insert(position, DIV(LABEL(a, _class='control-label col-sm-3'),
DIV(b, SPAN(c, _class='help-block'),
_class='col-sm-9'),
_class='form-group', _id=_id))
elif style == "bootstrap3_stacked":
form[0].insert(position, DIV(LABEL(a, _class='control-label'),
b, SPAN(c, _class='help-block'),
_class='form-group', _id=_id))
else: else:
form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'), form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'),
TD(b, _class='w2p_fw'), TD(b, _class='w2p_fw'),
@@ -1002,6 +1149,7 @@ class Auth(object):
reset_password_requires_verification=False, reset_password_requires_verification=False,
registration_requires_verification=False, registration_requires_verification=False,
registration_requires_approval=False, registration_requires_approval=False,
bulk_register_enabled=False,
login_after_registration=False, login_after_registration=False,
login_after_password_change=True, login_after_password_change=True,
alternate_requires_registration=False, alternate_requires_registration=False,
@@ -1035,6 +1183,7 @@ class Auth(object):
table_permission_name='auth_permission', table_permission_name='auth_permission',
table_event_name='auth_event', table_event_name='auth_event',
table_cas_name='auth_cas', table_cas_name='auth_cas',
table_token_name='auth_token',
table_user=None, table_user=None,
table_group=None, table_group=None,
table_membership=None, table_membership=None,
@@ -1103,6 +1252,8 @@ class Auth(object):
retrieve_password_subject='Password retrieve', retrieve_password_subject='Password retrieve',
reset_password='Click on the link %(link)s to reset your password', reset_password='Click on the link %(link)s to reset your password',
reset_password_subject='Password reset', reset_password_subject='Password reset',
bulk_invite_subject='Invitation to join%(site)s',
bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process',
invalid_reset_password='Invalid reset password', invalid_reset_password='Invalid reset password',
profile_updated='Profile updated', profile_updated='Profile updated',
new_password='New password', new_password='New password',
@@ -1316,6 +1467,7 @@ class Auth(object):
settings.update(Auth.default_settings) settings.update(Auth.default_settings)
settings.update( settings.update(
cas_domains=[request.env.http_host], cas_domains=[request.env.http_host],
enable_tokens=False,
cas_provider=cas_provider, cas_provider=cas_provider,
cas_actions=dict(login='login', cas_actions=dict(login='login',
validate='validate', validate='validate',
@@ -1330,8 +1482,7 @@ class Auth(object):
logged_url=URL(controller, function, args='profile'), logged_url=URL(controller, function, args='profile'),
download_url=URL(controller, 'download'), download_url=URL(controller, 'download'),
mailer=(mailer is True) and Mail() or mailer, mailer=(mailer is True) and Mail() or mailer,
on_failed_authorization = on_failed_authorization = URL(controller, function, args='not_authorized'),
URL(controller, function, args='not_authorized'),
login_next = url_index, login_next = url_index,
login_onvalidation = [], login_onvalidation = [],
login_onaccept = [], login_onaccept = [],
@@ -1356,6 +1507,8 @@ class Auth(object):
change_password_onvalidation = [], change_password_onvalidation = [],
change_password_onaccept = [], change_password_onaccept = [],
retrieve_password_onvalidation = [], retrieve_password_onvalidation = [],
request_reset_password_onvalidation = [],
request_reset_password_onaccept = [],
reset_password_onvalidation = [], reset_password_onvalidation = [],
reset_password_onaccept = [], reset_password_onaccept = [],
hmac_key = hmac_key, hmac_key = hmac_key,
@@ -1391,6 +1544,12 @@ class Auth(object):
next = current.request.vars._next next = current.request.vars._next
if isinstance(next, (list, tuple)): if isinstance(next, (list, tuple)):
next = next[0] next = next[0]
if next and self.settings.prevent_open_redirect_attacks:
# Prevent an attacker from adding an arbitrary url after the
# _next variable in the request.
items = next.split('/')
if '//' in next and items[2] != current.request.env.http_host:
next = None
return next return next
def _get_user_id(self): def _get_user_id(self):
@@ -1417,6 +1576,9 @@ class Auth(object):
def table_cas(self): def table_cas(self):
return self.db[self.settings.table_cas_name] return self.db[self.settings.table_cas_name]
def table_token(self):
return self.db[self.settings.table_token_name]
def _HTTP(self, *a, **b): def _HTTP(self, *a, **b):
""" """
only used in lambda: self._HTTP(404) only used in lambda: self._HTTP(404)
@@ -1444,7 +1606,8 @@ class Auth(object):
'retrieve_username', 'retrieve_password', 'retrieve_username', 'retrieve_password',
'reset_password', 'request_reset_password', 'reset_password', 'request_reset_password',
'change_password', 'profile', 'groups', 'change_password', 'profile', 'groups',
'impersonate', 'not_authorized'): 'impersonate', 'not_authorized', 'confirm_registration',
'bulk_register','manage_tokens'):
if len(request.args) >= 2 and args[0] == 'impersonate': if len(request.args) >= 2 and args[0] == 'impersonate':
return getattr(self, args[0])(request.args[1]) return getattr(self, args[0])(request.args[1])
else: else:
@@ -1771,7 +1934,7 @@ class Auth(object):
writable=False, readable=False, writable=False, readable=False,
label=T('Modified By'), ondelete=ondelete)) label=T('Modified By'), ondelete=ondelete))
def define_tables(self, username=None, signature=None, def define_tables(self, username=None, signature=None, enable_tokens=False,
migrate=None, fake_migrate=None): migrate=None, fake_migrate=None):
""" """
To be called unless tables are defined manually To be called unless tables are defined manually
@@ -1798,6 +1961,7 @@ class Auth(object):
username = settings.use_username username = settings.use_username
else: else:
settings.use_username = username settings.use_username = username
settings.enable_tokens = enable_tokens
if not self.signature: if not self.signature:
self.define_signature() self.define_signature()
if signature == True: if signature == True:
@@ -1981,6 +2145,21 @@ class Auth(object):
migrate=self.__get_migrate( migrate=self.__get_migrate(
settings.table_cas_name, migrate), settings.table_cas_name, migrate),
fake_migrate=fake_migrate)) fake_migrate=fake_migrate))
if settings.enable_tokens:
extra_fields = settings.extra_fields.get(
settings.table_token_name, []) + signature_list
if not settings.table_token_name in db.tables:
db.define_table(
settings.table_token_name,
Field('user_id', reference_table_user, default=None,
label=self.messages.label_user_id),
Field('expires_on', 'datetime', default=datetime.datetime(2999,12,31)),
Field('token',writable=False,default=web2py_uuid(),unique=True),
*extra_fields,
**dict(
migrate=self.__get_migrate(
settings.table_token_name, migrate),
fake_migrate=fake_migrate))
if not db._lazy_tables: if not db._lazy_tables:
settings.table_user = db[settings.table_user_name] settings.table_user = db[settings.table_user_name]
settings.table_group = db[settings.table_group_name] settings.table_group = db[settings.table_group_name]
@@ -2180,8 +2359,8 @@ class Auth(object):
# user not in database try other login methods # user not in database try other login methods
for login_method in self.settings.login_methods: for login_method in self.settings.login_methods:
if login_method != self and login_method(username, password): if login_method != self and login_method(username, password):
self.user = username self.user = user
return username return user
return False return False
def register_bare(self, **fields): def register_bare(self, **fields):
@@ -2190,14 +2369,16 @@ class Auth(object):
and a raw password. and a raw password.
""" """
settings = self._get_login_settings() settings = self._get_login_settings()
if not fields.get(settings.passfield): # users can register_bare even if no password is provided,
raise ValueError("register_bare: " + # in this case they will have to reset their password to login
"password not provided or invalid") if fields.get(settings.passfield):
elif not fields.get(settings.userfield): fields[settings.passfield] = \
settings.table_user[settings.passfield].validate(fields[settings.passfield])[0]
if not fields.get(settings.userfield):
raise ValueError("register_bare: " + raise ValueError("register_bare: " +
"userfield not provided or invalid") "userfield not provided or invalid")
fields[settings.passfield] = settings.table_user[settings.passfield].validate(fields[settings.passfield])[0] user = self.get_or_create_user(fields, login=False, get=False,
user = self.get_or_create_user(fields, login=False, get=False, update_fields=self.settings.update_fields) update_fields=self.settings.update_fields)
if not user: if not user:
# get or create did not create a user (it ignores duplicate records) # get or create did not create a user (it ignores duplicate records)
return False return False
@@ -2341,10 +2522,6 @@ class Auth(object):
### use session for federated login ### use session for federated login
snext = self.get_vars_next() snext = self.get_vars_next()
if snext and self.settings.prevent_open_redirect_attacks:
items = snext.split('/')
if '//' in snext and items[2] != request.env.http_host:
snext = None
if snext: if snext:
session._auth_next = snext session._auth_next = snext
@@ -2717,14 +2894,18 @@ class Auth(object):
passfield = self.settings.password_field passfield = self.settings.password_field
formstyle = self.settings.formstyle formstyle = self.settings.formstyle
if self.settings.register_verify_password: if self.settings.register_verify_password:
if self.settings.register_fields == None:
self.settings.register_fields = [f.name for f in table_user if f.writable]
k = self.settings.register_fields.index("password")
self.settings.register_fields.insert(k+1, "password_two")
extra_fields = [ extra_fields = [
Field("password_two", "password", requires=IS_EQUAL_TO( Field("password_two", "password", requires=IS_EQUAL_TO(
request.post_vars.get(passfield, None), request.post_vars.get(passfield, None),
error_message=self.messages.mismatched_password), error_message=self.messages.mismatched_password),
label=current.T("Confirm Password"))] label=current.T("Confirm Password"))]
else: else:
extra_fields = [] extra_fields = []
form = SQLFORM(table_user, form = SQLFORM(table_user,
fields=self.settings.register_fields, fields=self.settings.register_fields,
hidden=dict(_next=next), hidden=dict(_next=next),
@@ -2993,6 +3174,147 @@ class Auth(object):
table_user.email.requires = old_requires table_user.email.requires = old_requires
return form return form
def confirm_registration(
self,
next=DEFAULT,
onvalidation=DEFAULT,
onaccept=DEFAULT,
log=DEFAULT,
):
"""
Returns a form to confirm user registration
"""
table_user = self.table_user()
request = current.request
# response = current.response
session = current.session
if next is DEFAULT:
next = self.get_vars_next() or self.settings.reset_password_next
if self.settings.prevent_password_reset_attacks:
key = request.vars.key
if not key and len(request.args)>1:
key = request.args[-1]
if key:
session._reset_password_key = key
redirect(self.url(args='confirm_registration'))
else:
key = session._reset_password_key
else:
key = request.vars.key or getarg(-1)
try:
t0 = int(key.split('-')[0])
if time.time() - t0 > 60 * 60 * 24:
raise Exception
user = table_user(reset_password_key=key)
if not user:
raise Exception
except Exception as e:
session.flash = self.messages.invalid_reset_password
redirect(self.url('login', vars=dict(test=e)))
redirect(next, client_side=self.settings.client_side)
passfield = self.settings.password_field
form = SQLFORM.factory(
Field('first_name',
label='First Name',
required=True),
Field('last_name',
label='Last Name',
required=True),
Field('new_password', 'password',
label=self.messages.new_password,
requires=self.table_user()[passfield].requires),
Field('new_password2', 'password',
label=self.messages.verify_password,
requires=[IS_EXPR(
'value==%s' % repr(request.vars.new_password),
self.messages.mismatched_password)]),
submit_button='Confirm Registration',
hidden=dict(_next=next),
formstyle=self.settings.formstyle,
separator=self.settings.label_separator
)
if form.process().accepted:
user.update_record(
**{passfield: str(form.vars.new_password),
'first_name': str(form.vars.first_name),
'last_name': str(form.vars.last_name),
'registration_key': '',
'reset_password_key': ''})
session.flash = self.messages.password_changed
if self.settings.login_after_password_change:
self.login_user(user)
redirect(next, client_side=self.settings.client_side)
return form
def email_registration(self, subject, body, user):
"""
Sends and email invitation to a user informing they have been registered with the application
"""
reset_password_key = str(int(time.time())) + '-' + web2py_uuid()
link = self.url(self.settings.function,
args=('confirm_registration',), vars={'key': reset_password_key},
scheme=True)
d = dict(user)
d.update(dict(key=reset_password_key, link=link, site=current.request.env.http_host))
if self.settings.mailer and self.settings.mailer.send(
to=user.email,
subject=subject % d,
message=body % d):
user.update_record(reset_password_key=reset_password_key)
return True
return False
def bulk_register(self, max_emails=100):
"""
Creates a form for ther user to send invites to other users to join
"""
if not self.user:
redirect(self.settings.login_url)
if not self.setting.bulk_register_enabled:
return HTTP(404)
form = SQLFORM.factory(
Field('subject','string',default=self.messages.bulk_invite_subject,requires=IS_NOT_EMPTY()),
Field('emails','text',requires=IS_NOT_EMPTY()),
Field('message','text',default=self.messages.bulk_invite_body,requires=IS_NOT_EMPTY()),
formstyle=self.settings.formstyle)
if form.process().accepted:
emails = re.compile('[^\s\'"@<>,;:]+\@[^\s\'"@<>,;:]+').findall(form.vars.emails)
# send the invitations
emails_sent = []
emails_fail = []
emails_exist = []
for email in emails[:max_emails]:
if self.table_user()(email=email):
emails_exist.append(email)
else:
user = self.register_bare(email=email)
if self.email_registration(form.vars.subject, form.vars.message, user):
emails_sent.append(email)
else:
emails_fail.append(email)
emails_fail += emails[max_emails:]
form = DIV(H4('Emails sent'),UL(*[A(x,_href='mailto:'+x) for x in emails_sent]),
H4('Emails failed'),UL(*[A(x,_href='mailto:'+x) for x in emails_fail]),
H4('Emails existing'),UL(*[A(x,_href='mailto:'+x) for x in emails_exist]))
return form
def manage_tokens(self):
if not self.user:
redirect(self.settings.login_url)
table_token =self.table_token()
table_token.user_id.writable = False
table_token.user_id.default = self.user.id
table_token.token.writable = False
if current.request.args(1) == 'new':
table_token.token.readable = False
form = SQLFORM.grid(table_token, args=['manage_tokens'])
return form
def reset_password(self, def reset_password(self,
next=DEFAULT, next=DEFAULT,
onvalidation=DEFAULT, onvalidation=DEFAULT,
@@ -3030,6 +3352,12 @@ class Auth(object):
except Exception: except Exception:
session.flash = self.messages.invalid_reset_password session.flash = self.messages.invalid_reset_password
redirect(next, client_side=self.settings.client_side) redirect(next, client_side=self.settings.client_side)
if onvalidation is DEFAULT:
onvalidation = self.settings.reset_password_onvalidation
if onaccept is DEFAULT:
onaccept = self.settings.reset_password_onaccept
passfield = self.settings.password_field passfield = self.settings.password_field
form = SQLFORM.factory( form = SQLFORM.factory(
Field('new_password', 'password', Field('new_password', 'password',
@@ -3045,7 +3373,7 @@ class Auth(object):
formstyle=self.settings.formstyle, formstyle=self.settings.formstyle,
separator=self.settings.label_separator separator=self.settings.label_separator
) )
if form.accepts(request, session, if form.accepts(request, session, onvalidation=onvalidation,
hideerror=self.settings.hideerror): hideerror=self.settings.hideerror):
user.update_record( user.update_record(
**{passfield: str(form.vars.new_password), **{passfield: str(form.vars.new_password),
@@ -3054,6 +3382,7 @@ class Auth(object):
session.flash = self.messages.password_changed session.flash = self.messages.password_changed
if self.settings.login_after_password_change: if self.settings.login_after_password_change:
self.login_user(user) self.login_user(user)
callback(onaccept, form)
redirect(next, client_side=self.settings.client_side) redirect(next, client_side=self.settings.client_side)
return form return form
@@ -3079,9 +3408,9 @@ class Auth(object):
response.flash = self.messages.function_disabled response.flash = self.messages.function_disabled
return '' return ''
if onvalidation is DEFAULT: if onvalidation is DEFAULT:
onvalidation = self.settings.reset_password_onvalidation onvalidation = self.settings.request_reset_password_onvalidation
if onaccept is DEFAULT: if onaccept is DEFAULT:
onaccept = self.settings.reset_password_onaccept onaccept = self.settings.request_reset_password_onaccept
if log is DEFAULT: if log is DEFAULT:
log = self.messages['reset_password_log'] log = self.messages['reset_password_log']
userfield = self.settings.login_userfield or 'username' \ userfield = self.settings.login_userfield or 'username' \
@@ -3451,6 +3780,26 @@ class Auth(object):
""" """
return self.requires(True, otherwise=otherwise) return self.requires(True, otherwise=otherwise)
def requires_login_or_token(self, otherwise=None):
if self.settings.enable_tokens == True:
user = None
request = current.request
token = request.env.http_web2py_user_token or request.vars._token
table_token = self.table_token()
table_user = self.table_user()
from gluon.settings import global_settings
if global_settings.web2py_runtime_gae:
row = table_token(token=token)
if row:
user = table_user(row.user_id)
else:
row = self.db(table_token.token==token)(table_user.id==table_token.user_id).select().first()
if row:
user = row[table_user._tablename]
if user:
self.login_user(user)
return self.requires(True, otherwise=otherwise)
def requires_membership(self, role=None, group_id=None, otherwise=None): def requires_membership(self, role=None, group_id=None, otherwise=None):
""" """
Decorator that prevents access to action if not logged in or Decorator that prevents access to action if not logged in or
@@ -3571,7 +3920,7 @@ class Auth(object):
return record.id return record.id
else: else:
id = membership.insert(group_id=group_id, user_id=user_id) id = membership.insert(group_id=group_id, user_id=user_id)
if role: if role:
self.user_groups[group_id] = role self.user_groups[group_id] = role
else: else:
self.update_groups() self.update_groups()
@@ -5174,11 +5523,12 @@ def completion(callback):
return _completion return _completion
def prettydate(d, T=lambda x: x): def prettydate(d, T=lambda x: x, utc=False):
now = datetime.datetime.utcnow() if utc else datetime.datetime.now()
if isinstance(d, datetime.datetime): if isinstance(d, datetime.datetime):
dt = datetime.datetime.now() - d dt = now - d
elif isinstance(d, datetime.date): elif isinstance(d, datetime.date):
dt = datetime.date.today() - d dt = now.date() - d
elif not d: elif not d:
return '' return ''
else: else:
@@ -6049,7 +6399,7 @@ class Wiki(object):
count = db.wiki_tag.wiki_page.count() count = db.wiki_tag.wiki_page.count()
fields = [db.wiki_page.id, db.wiki_page.slug, fields = [db.wiki_page.id, db.wiki_page.slug,
db.wiki_page.title, db.wiki_page.tags, db.wiki_page.title, db.wiki_page.tags,
db.wiki_page.can_read] db.wiki_page.can_read, db.wiki+page.can_edit]
if preview: if preview:
fields.append(db.wiki_page.body) fields.append(db.wiki_page.body)
if query is None: if query is None:
+17 -12
View File
@@ -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:
@@ -3479,7 +3482,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 +3494,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 +3703,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 +3728,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,
+5
View File
@@ -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_:
+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
+1 -1
View File
@@ -301,7 +301,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;
+2 -2
View File
@@ -1,7 +1,7 @@
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.
@@ -84,7 +84,7 @@ 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>
+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