Compare commits

...

194 Commits

Author SHA1 Message Date
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
mdipierro
58533954dc R-2.10.4 2015-04-26 09:07:07 -05:00
mdipierro
236dc4b943 fixed submodule tracking 2015-04-26 09:04:55 -05:00
mdipierro
6612fd1cfe told git to track the right submodule branch 2015-04-26 08:50:16 -05:00
mdipierro
520950ba74 track track 15.03-maintenance (19/04/15) 2015-04-26 08:44:45 -05:00
mdipierro
e943aa9c25 minor compatibility fix in ldap_auth 2015-04-26 08:39:47 -05:00
mdipierro
756aec7206 Merge pull request #944 from stephenrauch/ldap_auth-fix-nosql
Fix ldap_auth for NoSQL databases
2015-04-26 08:29:26 -05:00
mdipierro
970e2ed35c Merge pull request #939 from smorrison/slack_webhook_support
add support for reporting web2py errors via slack.com
2015-04-26 08:29:00 -05:00
mdipierro
1388c39636 Merge pull request #935 from niphlod/docs/dal
extend underline for proper sphinx formatting
2015-04-26 08:27:48 -05:00
mdipierro
6e84737924 Merge pull request #934 from niphlod/fix/931
fixes #931 . Thanks @butsyk for spotting the bug
2015-04-26 08:27:35 -05:00
stephenrauch
0ad50630f2 Fix ldap_auth for NoSQL databases
Unroll query with join to two queries when working with DB's which don't
support joins.
2015-04-24 11:02:18 -07:00
Sean Morrison
f42ee15f5f add support for reporting web2py errors via slack.com 2015-04-22 19:00:21 -05:00
niphlod
77f154a56b added newer Recaptcha2 class to deal with v2.0. Fixes #919
Improvements over the "old" v1.0
- behaves well also without javascript
- use_ssl is redundant, v2.0 works only in https mode
- ajax is not useful anymore as the newer API is a lot easier

Adjusted also the addrow() method that was missing newer formstyles.
2015-04-22 00:10:05 +02:00
niphlod
2b0bfba649 extend underline for proper sphinx formatting 2015-04-21 23:59:07 +02:00
niphlod
9f1edf267d fixes #931 . Thanks @butsyk for spotting the bug 2015-04-21 21:59:42 +02:00
mdipierro
f3bda9ad02 changed version 2015-04-20 18:02:17 -05:00
mdipierro
f8afc76263 Merge pull request #930 from gi0baro/master
pydal -> track 15.03-maintenance (20/04/15)
2015-04-20 10:39:29 -05:00
gi0baro
65b4aaf842 pydal -> track 15.03-maintenance (20/04/15) 2015-04-20 17:29:22 +02:00
mdipierro
1b729cfbfc Merge pull request #928 from niphlod/fix/920
small typo. Fixes #920
2015-04-19 15:32:28 -05:00
mdipierro
1aa5f30091 Merge pull request #926 from niphlod/enhancement/web2py_on_iis
added web.config to deploy web2py on IIS
2015-04-19 15:32:11 -05:00
mdipierro
b17174c04c Merge pull request #925 from gi0baro/master
pydal -> track 15.03-maintenance (19/04/15)
2015-04-19 15:31:41 -05:00
niphlod
ac80adc9b4 small typo. Fixes #920 2015-04-19 19:49:25 +02:00
niphlod
888fa3dfc8 added setup script 2015-04-19 19:00:59 +02:00
niphlod
f3d815e84b added web.config to deploy web2py on IIS 2015-04-19 15:51:17 +02:00
gi0baro
9915fdf093 pydal -> track 15.03-maintenance (19/04/15) 2015-04-19 14:45:19 +02:00
mdipierro
ef8f802df9 R-2.10.4.beta 2015-04-18 15:44:04 -05:00
pallav_fdsi
842207ab33 Merge branch 'master' of https://github.com/web2py/web2py 2015-04-04 19:51:26 -04:00
pallav_fdsi
c58f29bb9c Merge branch 'master' of https://github.com/web2py/web2py 2015-01-25 15:58:39 -05:00
pallav_fdsi
0b0f82b514 Merge branch 'master' of https://github.com/web2py/web2py 2014-11-21 00:29:14 -05:00
pallav_fdsi
3ab8a7bfd6 Merge branch 'master' of https://github.com/web2py/web2py 2014-11-11 00:21:27 -05:00
pallav_fdsi
c5a9d2c456 Ignore the contents of the site-packages directory 2014-11-06 10:35:49 -05:00
66 changed files with 1621 additions and 364 deletions

1
.gitignore vendored
View File

@@ -58,3 +58,4 @@ HOWTO-web2py-devel
*.sublime-project
*.sublime-workspace
.idea/*
site-packages/

View File

@@ -17,14 +17,14 @@ install:
before_script:
- 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 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
after_success:
- 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:
email: true

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
- welcome app defaults to Bootstrap 3

View File

@@ -32,7 +32,7 @@ update:
echo "remember that pymysql was tweaked"
src:
### Use semantic versioning
echo 'Version 2.10.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
echo 'Version 2.12.1-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files
make clean
### clean up baisc apps

View File

@@ -13,7 +13,7 @@ Learn more at http://web2py.com
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.
@@ -38,9 +38,10 @@ PyDAL uses a separate stable release cycle to the rest of web2py. PyDAL releases
## 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
@@ -63,7 +64,7 @@ That's it!!!
packages/ > web2py submodules
dal/
contrib/ > third party libraries
tests/ > unittests
tests/ > unittests
applications/ > are the apps
admin/ > web based IDE
...

View File

@@ -1 +1 @@
Version 2.10.3-stable+timestamp.2015.04.02.16.28.49
Version 2.12.1-stable+timestamp.2015.08.07.02.10.57

View File

@@ -49,7 +49,8 @@ if request.function == 'manage':
auth.table_group(),
auth.table_permission()])
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
elif (request.application == 'admin' and not session.authorized) or \
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
@@ -80,7 +81,6 @@ if False and request.tickets_db:
def get_databases(request):
dbs = {}
for (key, value) in global_env.items():
cond = False
try:
cond = isinstance(value, GQLDB)
except:
@@ -420,7 +420,7 @@ def ccache():
'oldest': time.time(),
'keys': []
}
disk = copy.copy(ram)
total = copy.copy(ram)
disk['keys'] = []
@@ -480,12 +480,12 @@ def ccache():
disk['oldest'] = value[0]
disk['keys'].append((key, GetInHMS(time.time() - value[0])))
total['entries'] = ram['entries'] + disk['entries']
total['bytes'] = ram['bytes'] + disk['bytes']
total['objects'] = ram['objects'] + disk['objects']
total['hits'] = ram['hits'] + disk['hits']
total['misses'] = ram['misses'] + disk['misses']
total['keys'] = ram['keys'] + disk['keys']
ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
ram_keys.remove('ratio')
ram_keys.remove('oldest')
for key in ram_keys:
total[key] = ram[key] + disk[key]
try:
total['ratio'] = total['hits'] * 100 / (total['hits'] +
total['misses'])
@@ -577,9 +577,7 @@ def bg_graph_model():
group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group):
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename)
else:
subgraphs[group]['tables'].append(tablename)
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename))

View File

@@ -220,7 +220,7 @@ def list_breakpoints():
"Return a list of linenumbers for current breakpoints"
breakpoints = []
ok = None
ok = False
try:
filename = os.path.join(request.env['applications_parent'],
'applications', request.vars.filename)
@@ -235,5 +235,4 @@ def list_breakpoints():
ok = True
except Exception, e:
session.flash = str(e)
ok = False
return response.json({'ok': ok, 'breakpoints': breakpoints})

View File

@@ -292,9 +292,6 @@ def site():
log_progress(appname)
session.flash = T(msg, dict(appname=appname,
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:
msg = 'unable to install application "%(appname)s"'
session.flash = T(msg, dict(appname=form_update.vars.name))
@@ -370,25 +367,56 @@ def pack_plugin():
session.flash = T('internal error')
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():
app = get_app()
base = apath(app, r=request)
if request.post_vars.file:
files = request.post_vars.file
files = [files] if not isinstance(files,list) else files
fname = 'web2py.app.%s.w2p' % app
try:
filename = app_pack(app, request, raise_ex=True, filenames=files)
except Exception, e:
filename = None
if filename:
response.headers['Content-Type'] = 'application/w2p'
disposition = 'attachment; filename=%s' % fname
response.headers['Content-Disposition'] = disposition
return safe_read(filename, 'rb')
if request.post_vars.doexe is None:
fname = 'web2py.app.%s.w2p' % app
try:
filename = app_pack(app, request, raise_ex=True, filenames=files)
except Exception, e:
filename = None
if filename:
response.headers['Content-Type'] = 'application/w2p'
disposition = 'attachment; filename=%s' % fname
response.headers['Content-Disposition'] = disposition
return safe_read(filename, 'rb')
else:
session.flash = T('internal error: %s', e)
redirect(URL(args=request.args))
else:
session.flash = T('internal error: %s', e)
redirect(URL(args=request.args))
return pack_exe(app, base, files)
def ignore(fs):
return [f for f in fs if not (
f[:1] in '#' or f.endswith('~') or f.endswith('.bak'))]
@@ -744,7 +772,7 @@ def edit():
viewlist.append(aviewpath + '.html')
if len(viewlist):
editviewlinks = []
for v in viewlist:
for v in sorted(viewlist):
vf = os.path.split(v)[-1]
vargs = "/".join([viewpath.replace(os.sep, "/"), vf])
editviewlinks.append(A(vf.split(".")[0],
@@ -754,6 +782,7 @@ def edit():
if len(request.args) > 2 and request.args[1] == 'controllers':
controller = (request.args[2])[:-3]
functions = find_exposed_functions(data)
functions = functions and sorted(functions) or []
else:
(controller, functions) = (None, None)
@@ -866,13 +895,9 @@ def resolve():
def getclass(item):
""" Determine item class """
if item[0] == ' ':
return 'normal'
if item[0] == '+':
return 'plus'
if item[0] == '-':
return 'minus'
operators = {' ':'normal', '+':'plus', '-':'minus'}
return operators[item[0]]
if request.vars:
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:
data = safe_read(apath('%s/controllers/%s' % (app, c), r=request))
items = find_exposed_functions(data)
functions[c] = items
functions[c] = items and sorted(items) or []
# Get all views
views = sorted(
@@ -1205,7 +1230,7 @@ def plugin():
for c in controllers:
data = safe_read(apath('%s/controllers/%s' % (app, c), r=request))
items = find_exposed_functions(data)
functions[c] = items
functions[c] = items and sorted(items) or []
# Get all views
views = sorted(
@@ -1509,7 +1534,7 @@ def upload_file():
if filename:
d = dict(filename=filename[len(path):])
else:
d = dict(filename='unkown')
d = dict(filename='unknown')
session.flash = T('cannot upload file "%(filename)s"', d)
redirect(request.vars.sender)

File diff suppressed because one or more lines are too long

View File

@@ -490,12 +490,12 @@
* and prevent clicking on it */
disableElement: function(el) {
el.addClass('disabled');
var method = el.is('button') ? 'html' : 'val';
var method = el.is('input') ? 'val' : 'html';
//method = el.attr('name') ? 'html' : 'val';
var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state if not already disabled */
if(el.data('w2p:enable-with') === undefined) {
el.data('w2p:enable-with', el[method]());
if(el.data('w2p_enable_with') === undefined) {
el.data('w2p_enable_with', el[method]());
}
/*if you don't want to see "working..." on buttons, replace the following
* two lines with this one
@@ -515,11 +515,11 @@
/* restore element to its original state which was disabled by 'disableElement' above*/
enableElement: function(el) {
var method = el.is('button') ? 'val' : 'html';
if(el.data('w2p:enable-with') !== undefined) {
var method = el.is('input') ? 'val' : 'html';
if(el.data('w2p_enable_with') !== undefined) {
/* set to old enabled state */
el[method](el.data('w2p:enable-with'));
el.removeData('w2p:enable-with');
el[method](el.data('w2p_enable_with'));
el.removeData('w2p_enable_with');
}
el.removeClass('disabled');
el.unbind('click.w2pDisable');
@@ -586,12 +586,14 @@
if(pre_call != undefined) {
eval(pre_call);
}
if(confirm_message != undefined) {
if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?';
if(!web2py.confirm(confirm_message)) {
web2py.stopEverything(e);
return;
}
if(confirm_message) {
if(confirm_message == 'default')
confirm_message = w2p_ajax_confirm_message ||
'Are you sure you want to delete this object?';
if(!web2py.confirm(confirm_message)) {
web2py.stopEverything(e);
return;
}
}
if(target == undefined) {
if(method == 'GET') {
@@ -634,7 +636,7 @@
});
},
/* 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
- Sets disabled property to true
*/
@@ -646,8 +648,8 @@
if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]())
}
if(element.data('w2p:enable-with') === undefined) {
element.data('w2p:enable-with', element[method]());
if(element.data('w2p_enable_with') === undefined) {
element.data('w2p_enable_with', element[method]());
}
element[method](element.data('w2p_disable_with'));
element.prop('disabled', true);
@@ -655,16 +657,16 @@
},
/* 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
*/
enableFormElements: function(form) {
form.find(web2py.enableSelector).each(function() {
var element = $(this),
method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) {
element[method](element.data('w2p:enable-with'));
element.removeData('w2p:enable-with');
if(element.data('w2p_enable_with')) {
element[method](element.data('w2p_enable_with'));
element.removeData('w2p_enable_with');
}
element.prop('disabled', false);
});
@@ -730,4 +732,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers;
web2py_trap_link = jQuery.web2py.trap_link;
web2py_calc_entropy = jQuery.web2py.calc_entropy;
*/
/* compatibility code - end*/
/* compatibility code - end*/

View File

@@ -1,5 +1,7 @@
{{extend 'layout.html'}}
{{
import re
regex_space = re.compile('\s+')
def all(items):
return reduce(lambda a,b:a and b,items,True)
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:
if len(file_path)>=len(path) and all([v==file_path[k] for k,v in enumerate(path)]):
path.append(file_path[len(path)])
thispath='static__'+'__'.join(path)
thispath = regex_space.sub('-', 'static__'+'__'.join(path))
}}
<li class="folder"><i>&nbsp;</i>
<a href="javascript:collapse('{{=thispath}}');" class="file">{{=path[-1]}}/</a>

View File

@@ -25,6 +25,7 @@
<form action="{{=URL(args=request.args)}}" method="POST">
<h2>{{=T('Select Files to Package')}}</h2>
<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">
{{tree(base)}}
</div>

View File

@@ -49,7 +49,8 @@ if request.function == 'manage':
auth.table_group(),
auth.table_permission()])
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
elif (request.application == 'admin' and not session.authorized) or \
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
@@ -80,7 +81,6 @@ if False and request.tickets_db:
def get_databases(request):
dbs = {}
for (key, value) in global_env.items():
cond = False
try:
cond = isinstance(value, GQLDB)
except:
@@ -420,7 +420,7 @@ def ccache():
'oldest': time.time(),
'keys': []
}
disk = copy.copy(ram)
total = copy.copy(ram)
disk['keys'] = []
@@ -480,12 +480,12 @@ def ccache():
disk['oldest'] = value[0]
disk['keys'].append((key, GetInHMS(time.time() - value[0])))
total['entries'] = ram['entries'] + disk['entries']
total['bytes'] = ram['bytes'] + disk['bytes']
total['objects'] = ram['objects'] + disk['objects']
total['hits'] = ram['hits'] + disk['hits']
total['misses'] = ram['misses'] + disk['misses']
total['keys'] = ram['keys'] + disk['keys']
ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
ram_keys.remove('ratio')
ram_keys.remove('oldest')
for key in ram_keys:
total[key] = ram[key] + disk[key]
try:
total['ratio'] = total['hits'] * 100 / (total['hits'] +
total['misses'])
@@ -577,9 +577,7 @@ def bg_graph_model():
group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group):
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename)
else:
subgraphs[group]['tables'].append(tablename)
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename))

View File

@@ -8,6 +8,7 @@
#### Learning and Demos
- [[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/]]
- [[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)
@@ -22,5 +23,6 @@
- [[More Plugins http://dev.s-cubism.com/web2py_plugins]]
- [[Appliances http://www.web2py.com/appliances 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]]

File diff suppressed because one or more lines are too long

View File

@@ -490,12 +490,12 @@
* and prevent clicking on it */
disableElement: function(el) {
el.addClass('disabled');
var method = el.is('button') ? 'html' : 'val';
var method = el.is('input') ? 'val' : 'html';
//method = el.attr('name') ? 'html' : 'val';
var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state if not already disabled */
if(el.data('w2p:enable-with') === undefined) {
el.data('w2p:enable-with', el[method]());
if(el.data('w2p_enable_with') === undefined) {
el.data('w2p_enable_with', el[method]());
}
/*if you don't want to see "working..." on buttons, replace the following
* two lines with this one
@@ -515,11 +515,11 @@
/* restore element to its original state which was disabled by 'disableElement' above*/
enableElement: function(el) {
var method = el.is('button') ? 'val' : 'html';
if(el.data('w2p:enable-with') !== undefined) {
var method = el.is('input') ? 'val' : 'html';
if(el.data('w2p_enable_with') !== undefined) {
/* set to old enabled state */
el[method](el.data('w2p:enable-with'));
el.removeData('w2p:enable-with');
el[method](el.data('w2p_enable_with'));
el.removeData('w2p_enable_with');
}
el.removeClass('disabled');
el.unbind('click.w2pDisable');
@@ -586,12 +586,14 @@
if(pre_call != undefined) {
eval(pre_call);
}
if(confirm_message != undefined) {
if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?';
if(!web2py.confirm(confirm_message)) {
web2py.stopEverything(e);
return;
}
if(confirm_message) {
if(confirm_message == 'default')
confirm_message = w2p_ajax_confirm_message ||
'Are you sure you want to delete this object?';
if(!web2py.confirm(confirm_message)) {
web2py.stopEverything(e);
return;
}
}
if(target == undefined) {
if(method == 'GET') {
@@ -634,7 +636,7 @@
});
},
/* 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
- Sets disabled property to true
*/
@@ -646,8 +648,8 @@
if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]())
}
if(element.data('w2p:enable-with') === undefined) {
element.data('w2p:enable-with', element[method]());
if(element.data('w2p_enable_with') === undefined) {
element.data('w2p_enable_with', element[method]());
}
element[method](element.data('w2p_disable_with'));
element.prop('disabled', true);
@@ -655,16 +657,16 @@
},
/* 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
*/
enableFormElements: function(form) {
form.find(web2py.enableSelector).each(function() {
var element = $(this),
method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) {
element[method](element.data('w2p:enable-with'));
element.removeData('w2p:enable-with');
if(element.data('w2p_enable_with')) {
element[method](element.data('w2p_enable_with'));
element.removeData('w2p_enable_with');
}
element.prop('disabled', false);
});
@@ -730,4 +732,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers;
web2py_trap_link = jQuery.web2py.trap_link;
web2py_calc_entropy = jQuery.web2py.calc_entropy;
*/
/* compatibility code - end*/
/* compatibility code - end*/

View File

@@ -36,7 +36,7 @@
</center>
<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>.
</p>

View File

@@ -17,19 +17,24 @@
<ul>
<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.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.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.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="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://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://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://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.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="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>

View File

@@ -49,7 +49,8 @@ if request.function == 'manage':
auth.table_group(),
auth.table_permission()])
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
elif (request.application == 'admin' and not session.authorized) or \
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
@@ -80,7 +81,6 @@ if False and request.tickets_db:
def get_databases(request):
dbs = {}
for (key, value) in global_env.items():
cond = False
try:
cond = isinstance(value, GQLDB)
except:
@@ -420,7 +420,7 @@ def ccache():
'oldest': time.time(),
'keys': []
}
disk = copy.copy(ram)
total = copy.copy(ram)
disk['keys'] = []
@@ -480,12 +480,12 @@ def ccache():
disk['oldest'] = value[0]
disk['keys'].append((key, GetInHMS(time.time() - value[0])))
total['entries'] = ram['entries'] + disk['entries']
total['bytes'] = ram['bytes'] + disk['bytes']
total['objects'] = ram['objects'] + disk['objects']
total['hits'] = ram['hits'] + disk['hits']
total['misses'] = ram['misses'] + disk['misses']
total['keys'] = ram['keys'] + disk['keys']
ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
ram_keys.remove('ratio')
ram_keys.remove('oldest')
for key in ram_keys:
total[key] = ram[key] + disk[key]
try:
total['ratio'] = total['hits'] * 100 / (total['hits'] +
total['misses'])
@@ -577,9 +577,7 @@ def bg_graph_model():
group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group):
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename)
else:
subgraphs[group]['tables'].append(tablename)
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename))

View File

@@ -30,6 +30,7 @@ def user():
http://..../[app]/default/user/retrieve_password
http://..../[app]/default/user/change_password
http://..../[app]/default/user/manage_users (requires membership in
http://..../[app]/default/user/bulk_register
use @auth.requires_login()
@auth.requires_membership('group name')
@auth.requires_permission('read','table name',record_id)

View File

@@ -62,7 +62,7 @@ auth.define_tables(username=False, signature=False)
## configure email
mail = auth.settings.mailer
mail.settings.server = 'logging' if request.is_local else myconf.take('smtp.sender')
mail.settings.server = 'logging' if request.is_local else myconf.take('smtp.server')
mail.settings.sender = myconf.take('smtp.sender')
mail.settings.login = myconf.take('smtp.login')

View File

@@ -7,11 +7,11 @@
# Language from default.py or 'en' (if the file is not found) is used as
# 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:
#
# 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
# (where APP - is your application directory)
# 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

View File

@@ -24,7 +24,9 @@ div.flash.alert:hover {
.ie-lte8 div.flash:hover {
filter: alpha(opacity=25);
}
.main-container {
margin-top: 20px;
}
div.error {
width: auto;
@@ -283,6 +285,7 @@ li.w2p_grid_breadcrumb_elem {
.web2py_console .form-control {
width: 20%;
display: inline;
height: 100%;
}
.web2py_console #w2p_keywords {
width: 50%;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -586,12 +586,14 @@
if(pre_call != undefined) {
eval(pre_call);
}
if(confirm_message != undefined) {
if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?';
if(!web2py.confirm(confirm_message)) {
web2py.stopEverything(e);
return;
}
if(confirm_message) {
if(confirm_message == 'default')
confirm_message = w2p_ajax_confirm_message ||
'Are you sure you want to delete this object?';
if(!web2py.confirm(confirm_message)) {
web2py.stopEverything(e);
return;
}
}
if(target == undefined) {
if(method == 'GET') {

View File

@@ -75,7 +75,7 @@
{{end}}
<!-- Main ========================================= -->
<!-- Begin page content -->
<div class="container-fluid">
<div class="container-fluid main-container">
{{if left_sidebar_enabled:}}
<div class="col-md-3 left-sidebar">
{{block left_sidebar}}

25
appveyor.yml Normal file
View File

@@ -0,0 +1,25 @@
build: false
environment:
matrix:
- PYTHON: "C:/Python27"
COVERAGE_PROCESS_START: gluon/tests/coverage.ini
clone_depth: 50
init:
- "ECHO %PYTHON%"
- set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%
install:
- ps: Start-FileDownload https://bootstrap.pypa.io/get-pip.py
- python get-pip.py
- pip install codecov
- git submodule update --init --recursive
test_script:
- python web2py.py --run_system_tests --with_coverage
after_test:
- coverage combine
- codecov

46
examples/web.config Normal file
View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- app configuration for web2py on IIS -->
<configuration>
<appSettings>
<add key="WSGI_HANDLER" value="gluon.main.wsgibase" />
<add key="WSGI_RESTART_FILE_REGEX" value=".*((routes\.py)|(\.config))$" />
</appSettings>
<system.webServer>
<rewrite>
<rules>
<clear />
<rule name="static" enabled="true" stopProcessing="true">
<match url="^(\w+)/static(?:/_[\d]+\.[\d]+\.[\d]+)?/(.*)$" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
<action type="Rewrite" url="applications/{R:1}/static/{R:2}" logRewrittenUrl="false" />
</rule>
<rule name="web2py_app" enabled="true" stopProcessing="true">
<match url="(.*)" ignoreCase="false" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
<action type="Rewrite" url="handler.web2py/{R:1}" appendQueryString="true" />
</rule>
</rules>
<outboundRules>
<rule name="static_version_cache_control" preCondition="static_version">
<match serverVariable="RESPONSE_Cache-Control" pattern=".*" />
<action type="Rewrite" value="max-age=315360000" />
<conditions>
</conditions>
</rule>
<rule name="static_version_Expires" preCondition="static_version">
<match serverVariable="RESPONSE_Expires" pattern=".*" />
<action type="Rewrite" value="Thu, 31 Dec 2037 23:59:59 GMT" />
</rule>
<preConditions>
<preCondition name="static_version">
<add input="{REQUEST_URI}" pattern="(\w+)/static(?:/_[\d]+\.[\d]+\.[\d]+)?/(.*)$" />
</preCondition>
</preConditions>
</outboundRules>
</rewrite>
<handlers>
<!-- replace SCRIPT_PROCESSOR with the configured handler for python -->
<add name="Python_via_FastCGI" path="handler.web2py" verb="*" modules="FastCgiModule" scriptProcessor="SCRIPT_PROCESSOR" resourceType="Unspecified" requireAccess="Script" />
</handlers>
</system.webServer>
</configuration>

View File

@@ -473,9 +473,14 @@ class CacheOnDisk(CacheAbstract):
if item and ((dt is None) or (item[0] > now - dt)):
value = item[1]
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.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})
self.storage.release(CacheAbstract.cache_stats_name)

View File

@@ -261,7 +261,7 @@ class LoadFactory(object):
import globals
target = target or 'c' + str(random.random())[2:]
attr['_id'] = target
request = self.environment['request']
request = current.request
if '.' in f:
f, extension = f.rsplit('.', 1)
if url or ajax:
@@ -532,10 +532,11 @@ def run_models_in(environment):
It tries pre-compiled models first before compiling them.
"""
folder = environment['request'].folder
c = environment['request'].controller
request = current.request
folder = request.folder
c = request.controller
#f = environment['request'].function
response = environment['response']
response = current.response
path = pjoin(folder, 'models')
cpath = pjoin(folder, 'compiled')
@@ -577,7 +578,7 @@ def run_controller_in(controller, function, environment):
"""
# if compiled should run compiled!
folder = environment['request'].folder
folder = current.request.folder
path = pjoin(folder, 'compiled')
badc = 'invalid controller (%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
code = getcfs(layer, filename, lambda: compile2(code, layer))
restricted(code, environment, filename)
response = environment['response']
response = current.response
vars = response._vars
if response.postprocessing:
vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars)
@@ -649,8 +650,8 @@ def run_view_in(environment):
or `view/generic.extension`
It tries the pre-compiled views_controller_function.pyc before compiling it.
"""
request = environment['request']
response = environment['response']
request = current.request
response = current.response
view = response.view
folder = request.folder
path = pjoin(folder, 'compiled')

View File

@@ -33,6 +33,7 @@ def ldap_auth(server='ldap', port=None,
group_name_attrib='cn',
group_member_attrib='memberUid',
group_filterstr='objectClass=*',
tls=False,
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
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
search it then specify bind_dn & bind_pw to use for this.
- currently only implemented for Active Directory
@@ -521,9 +529,19 @@ def ldap_auth(server='ldap', port=None,
logging.error(
'There is no username or email for %s!' % username)
raise
db_group_search = db((db.auth_membership.user_id == db_user_id) &
(db.auth_user.id == db.auth_membership.user_id) &
(db.auth_group.id == db.auth_membership.group_id))
# if old pydal version, assume this is a relational database which can do joins
db_can_join = db.can_join() if hasattr(db, 'can_join') else True
if db_can_join:
db_group_search = db(
(db.auth_membership.user_id == db_user_id) &
(db.auth_user.id == db.auth_membership.user_id) &
(db.auth_group.id == db.auth_membership.group_id))
else:
# no joins on NoSQL databases, perform two queries
db_group_search = db(db.auth_membership.user_id == db_user_id)
group_ids = [x.group_id for x in db_group_search.select(
db.auth_membership.group_id, distinct=True)]
db_group_search = db(db.auth_group.id.belongs(group_ids))
db_groups_of_the_user = list()
db_group_id = dict()
@@ -600,6 +618,8 @@ def ldap_auth(server='ldap', port=None,
ldap_port = 389
con = ldap.initialize(
"ldap://" + ldap_server + ":" + str(ldap_port))
if tls:
con.start_tls_s()
return con
def get_user_groups_from_ldap(username,

View File

@@ -67,11 +67,12 @@ def RedisCache(*args, **vars):
locker.acquire()
try:
if not hasattr(RedisCache, 'redis_instance'):
RedisCache.redis_instance = RedisClient(*args, **vars)
instance_name = 'redis_instance_' + current.request.application
if not hasattr(RedisCache, instance_name):
setattr(RedisCache, instance_name, RedisClient(*args, **vars))
return getattr(RedisCache, instance_name)
finally:
locker.release()
return RedisCache.redis_instance
class RedisClient(object):

View File

@@ -126,7 +126,7 @@ class History:
def globals_dict(self):
"""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))
def add_unpicklable(self, statement, names):

View File

@@ -145,6 +145,10 @@ class TokenHandler(tornado.web.RequestHandler):
class DistributeHandler(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
return True
def open(self, params):
group, token, name = params.split('/') + [None, None]
self.group = group or 'default'

View File

@@ -7,12 +7,12 @@
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
Takes care of adapting pyDAL to web2py's needs
--------------------------------------------
-----------------------------------------------
"""
from pydal import DAL as DAL
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

View File

@@ -26,6 +26,8 @@ import gluon.settings as settings
from gluon.utils import web2py_uuid, secure_dumps, secure_loads
from gluon.settings import global_settings
from gluon import recfile
from gluon.cache import CacheInRam
from gluon.fileutils import copystream
import hashlib
import portalocker
try:
@@ -47,8 +49,7 @@ import cgi
import urlparse
import copy
import tempfile
from gluon.cache import CacheInRam
from gluon.fileutils import copystream
FMT = '%a, %d-%b-%Y %H:%M:%S PST'
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>'
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:
# this is required so that pickled dict(s) and class.__dict__
# are sorted and web2py can detect without ambiguity when a session changes
class SortingPickler(Pickler):
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._batch_setitems([(key, obj[key]) for key in sorted(obj)])
@@ -193,6 +203,7 @@ class Request(Storage):
self.is_https = False
self.is_local = False
self.global_settings = settings.global_settings
self._uuid = None
def parse_get_vars(self):
"""Takes the QUERY_STRING and unpacks it to get_vars
@@ -275,7 +286,7 @@ class Request(Storage):
"""
self._vars = copy.copy(self.get_vars)
for key, value in self.post_vars.iteritems():
if not key in self._vars:
if key not in self._vars:
self._vars[key] = value
else:
if not isinstance(self._vars[key], list):
@@ -306,13 +317,21 @@ class Request(Storage):
self.parse_all_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):
self.uuid = '%s/%s.%s.%s' % (
self._uuid = '%s/%s.%s.%s' % (
self.application,
self.client.replace(':', '_'),
self.now.strftime('%Y-%m-%d.%H-%M-%S'),
web2py_uuid())
return self.uuid
return self._uuid
def user_agent(self):
from gluon.contrib import user_agent_parser
@@ -436,29 +455,32 @@ class Response(Storage):
return page
def include_meta(self):
s = "\n";
s = "\n"
for meta in (self.meta or {}).iteritems():
k, v = meta
if isinstance(v,dict):
s = s+'<meta'+''.join(' %s="%s"' % (xmlescape(key), xmlescape(v[key])) for key in v) +' />\n'
if isinstance(v, dict):
s += '<meta' + ''.join(' %s="%s"' % (xmlescape(key), xmlescape(v[key])) for key in v) +' />\n'
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)
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,
response.cache_includes = (cache_method, time_expire).
Example: (cache.disk, 60) # caches to disk for 1 minute.
"""
from gluon import URL
files = []
ext_files = []
has_js = has_css = False
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
if item in files:
continue
@@ -487,10 +509,13 @@ class Response(Storage):
time_expire)
else:
files = call_minify()
s = ''
files.extend(ext_files)
s = []
for item in files:
if isinstance(item, str):
f = item.lower().split('?')[0]
ext = f.rpartition('.')[2]
# if static_version we need also to check for
# static_version_urls. In that case, the _.x.x.x
# 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:
item = item.replace(
'/static/', '/static/_%s/' % self.static_version, 1)
if f.endswith('.css'):
s += css_template % item
elif f.endswith('.js'):
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
tmpl = template_mapping.get(ext)
if tmpl:
s.append(tmpl % item)
elif isinstance(item, (list, tuple)):
f = item[0]
if f == 'css:inline':
s += css_inline % item[1]
elif f == 'js:inline':
s += js_inline % item[1]
self.write(s, escape=False)
tmpl = template_mapping.get(f)
if tmpl:
s.append(tmpl % item[1])
self.write(''.join(s), escape=False)
def stream(self,
stream,
@@ -663,7 +679,7 @@ class Response(Storage):
return handler(request, self, methods)
def toolbar(self):
from html import DIV, SCRIPT, BEAUTIFY, TAG, URL, A
from gluon.html import DIV, SCRIPT, BEAUTIFY, TAG, A
BUTTON = TAG.button
admin = URL("admin", "default", "design", extension='html',
args=current.request.application)

View File

@@ -376,7 +376,6 @@ def wsgibase(environ, responder):
request.env.http_x_forwarded_proto in HTTPS_SCHEMES \
or env.https == 'on'
)
request.compute_uuid() # requires client
request.url = environ['PATH_INFO']
# ##################################################

View File

@@ -119,7 +119,7 @@ class LockedFile(object):
lock(self.file, LOCK_EX)
if not 'a' in mode:
self.file.seek(0)
self.file.truncate()
self.file.truncate(0)
else:
raise RuntimeError("invalid LockedFile(...,mode)")

View File

@@ -66,14 +66,15 @@ class XssCleaner(HTMLParser):
#to strip or escape disallowed tags?
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):
if data and not self.in_disallowed:
if data and not self.in_disallowed[-1]:
self.result += xssescape(data)
def handle_charref(self, ref):
if self.in_disallowed:
if self.in_disallowed[-1]:
return
elif len(ref) < 7 and (ref.isdigit() or ref == 'x27'): # x27 is a special case for apostrophe
self.result += '&#%s;' % ref
@@ -81,7 +82,7 @@ class XssCleaner(HTMLParser):
self.result += xssescape('&#%s' % ref)
def handle_entityref(self, ref):
if self.in_disallowed:
if self.in_disallowed[-1]:
return
elif ref in entitydefs:
self.result += '&%s;' % ref
@@ -89,7 +90,7 @@ class XssCleaner(HTMLParser):
self.result += xssescape('&%s' % ref)
def handle_comment(self, comment):
if self.in_disallowed:
if self.in_disallowed[-1]:
return
elif comment:
self.result += xssescape('<!--%s-->' % comment)
@@ -100,11 +101,11 @@ class XssCleaner(HTMLParser):
attrs
):
if tag not in self.permitted_tags:
if self.strip_disallowed:
self.in_disallowed = True
else:
self.in_disallowed.append(True)
if (not self.strip_disallowed):
self.result += xssescape('<%s>' % tag)
else:
self.in_disallowed.append(False)
bt = '<' + tag
if tag in self.allowed_attributes:
attrs = dict(attrs)
@@ -119,6 +120,7 @@ class XssCleaner(HTMLParser):
else:
bt += ' %s=%s' % (xssescape(attribute),
quoteattr(attrs[attribute]))
# deal with <a> without href and <img> without src
if bt == '<a' or bt == '<img':
return
if tag in self.requires_no_close:
@@ -129,10 +131,9 @@ class XssCleaner(HTMLParser):
def handle_endtag(self, tag):
bracketed = '</%s>' % tag
self.in_disallowed.pop()
if tag not in self.permitted_tags:
if self.strip_disallowed:
self.in_disallowed = False
else:
if (not self.strip_disallowed):
self.result += xssescape(bracketed)
elif tag in self.open_tags:
self.result += bracketed
@@ -143,10 +144,13 @@ class XssCleaner(HTMLParser):
Accepts relative, absolute, and mailto urls
"""
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('/'))
if url.startswith('#'):
return True
else:
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):
"""

View File

@@ -41,9 +41,7 @@ def enable_autocomplete_and_history(adir, env):
except ImportError:
pass
else:
readline.parse_and_bind("bind ^I rl_complete"
if sys.platform == 'darwin'
else "tab: complete")
readline.parse_and_bind("tab: complete")
history_file = os.path.join(adir, '.pythonhistory')
try:
readline.read_history_file(history_file)

View File

@@ -29,7 +29,7 @@ from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY
from pydal.base import DEFAULT
from pydal.objects import Table, Row, Expression, Field
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 gluon.storage import Storage
from gluon.utils import md5_hash
@@ -71,6 +71,26 @@ def represent(field, value, record):
else:
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):
try:
@@ -859,14 +879,14 @@ def formstyle_bootstrap3_stacked(form, fields):
label = ''
elif isinstance(controls, (SELECT, TEXTAREA)):
controls.add_class('form-control')
elif isinstance(controls, SPAN):
_controls = P(controls.components)
elif isinstance(controls, UL):
for e in controls.elements("input"):
e.add_class('form-control')
if isinstance(label, LABEL):
label['_class'] = 'control-label'
@@ -909,9 +929,9 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3):
label = ''
elif isinstance(controls, (SELECT, TEXTAREA)):
controls.add_class('form-control')
elif isinstance(controls, SPAN):
_controls = P(controls.components,
_controls = P(controls.components,
_class="form-control-static %s" % col_class)
elif isinstance(controls, UL):
for e in controls.elements("input"):
@@ -1126,7 +1146,8 @@ class SQLFORM(FORM):
extra_fields = extra_fields or []
self.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
extra_field.db = table._db
extra_field.table = table
@@ -1678,7 +1699,7 @@ class SQLFORM(FORM):
self.vars.update(pk)
else:
ret = False
else:
elif self.table._db._uri:
if record_id:
self.vars.id = self.record[self.id_field_name]
if fields:
@@ -1691,6 +1712,7 @@ class SQLFORM(FORM):
AUTOTYPES = {
type(''): ('string', None),
type(u''): ('string',None),
type(True): ('boolean', None),
type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)),
type(1.0): ('double', IS_FLOAT_IN_RANGE()),
@@ -1755,10 +1777,16 @@ class SQLFORM(FORM):
keywords = keywords[0]
request.vars.keywords = keywords
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')
parts = [field.contains(
key) for field in fields if field.type in SEARCHABLE_TYPES]
sfields = [field 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
# needs testing...
@@ -1772,10 +1800,6 @@ class SQLFORM(FORM):
# filters.append(reduce(lambda a, b: (a & b), all_words_filters))
#parts = filters
else:
parts = None
if parts:
return reduce(lambda a, b: a | b, parts)
else:
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')
_id = "%s_%s" % (value_id, name)
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':
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)
iso_format = {'_data-w2p_date_format': '%Y-%m-%d'}
widget_ = SQLFORM.widgets.date
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format)
elif field_type == 'datetime':
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)
iso_format = {'_data-w2p_datetime_format': '%Y-%m-%d %H:%M:%S'}
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
field_type.startswith('list:reference ')) and \
hasattr(field.requires, 'options') or \
hasattr(field.requires, 'options'):
value_input = SELECT(
*[OPTION(v, _value=k)
@@ -1856,7 +1884,8 @@ class SQLFORM(FORM):
elif field_type.startswith('reference ') or \
field_type.startswith('list:integer') or \
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:
value_input = INPUT(
_type='text', _id=_id,
@@ -1967,7 +1996,8 @@ class SQLFORM(FORM):
cache_count=None,
client_side_delete=False,
ignore_common_filters=None,
auto_pagination=True):
auto_pagination=True,
use_cursor=False):
formstyle = formstyle or current.response.formstyle
@@ -2069,18 +2099,15 @@ class SQLFORM(FORM):
# is unique and usually indexed. See issue #679
if not orderby:
orderby = field_id
else:
if isinstance(orderby, Expression):
if orderby.first:
# here we're with a DESC order on a field
# stored as orderby.first
if orderby.first is not field_id:
orderby = orderby | field_id
else:
# here we're with an ASC order on a field
# stored as orderby
if orderby is not field_id:
orderby = orderby | field_id
elif isinstance(orderby, list):
orderby = reduce(lambda a,b: a|b, orderby)
elif isinstance(orderby, Field) and orderby is not field_id:
# here we're with an ASC order on a field stored as orderby
orderby = orderby | field_id
elif (isinstance(orderby, Expression) and
orderby.first and orderby.first is not field_id):
# here we're with a DESC order on a field stored as orderby.first
orderby = orderby | field_id
return orderby
def url(**b):
@@ -2108,10 +2135,8 @@ class SQLFORM(FORM):
# - url has valid signature (vars are not signed, only path_info)
# = url does not contain 'create','delete','edit' (readonly)
if user_signature:
if not (
'/'.join(str(a) for a in args) == '/'.join(request.args) or
URL.verify(request, user_signature=user_signature,
hash_vars=False) or
if not ('/'.join(map(str,args)) == '/'.join(map(str,request.args)) or
URL.verify(request, user_signature=user_signature, hash_vars=False) or
(request.args(len(args)) == 'view' and not logged)):
session.flash = T('not authorized')
redirect(referrer)
@@ -2542,7 +2567,7 @@ class SQLFORM(FORM):
cursor = True
# 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
limitby = (0, paginate)
try:
@@ -2564,7 +2589,7 @@ class SQLFORM(FORM):
table_fields = [field for field in fields
if (field.tablename in tablenames and
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,
groupby=groupby, limitby=limitby,
reusecursor=cursor,
@@ -2574,6 +2599,7 @@ class SQLFORM(FORM):
rows = dbset.select(left=left, orderby=orderby,
groupby=groupby, limitby=limitby,
cacheable=True, *table_fields)
next_cursor = None
except SyntaxError:
rows = None
next_cursor = None
@@ -2592,7 +2618,7 @@ class SQLFORM(FORM):
console.append(DIV(message or '', _class='web2py_counter'))
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.
try:
page = int(request.vars.page or 1) - 1
@@ -2663,7 +2689,7 @@ class SQLFORM(FORM):
htmltable = TABLE(COLGROUP(*cols), THEAD(head))
tbody = TBODY()
numrec = 0
repr_cache = {}
repr_cache = CacheRepresenter()
for row in rows:
trcols = []
id = row[field_id]
@@ -2676,31 +2702,20 @@ class SQLFORM(FORM):
continue
if field.type == 'blob':
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)
if field.represent:
if field.type.startswith('reference'):
if field not in repr_cache:
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
nvalue = repr_cache(field, value, row)
else:
try:
nvalue = field.represent(value, row)
except KeyError:
try:
nvalue = field.represent(
value, row[field.tablename])
nvalue = field.represent(value, row[field.tablename])
except KeyError:
nvalue = None
value = nvalue

View File

@@ -269,31 +269,33 @@ class FastStorage(dict):
class List(list):
"""
Like a regular python list but a[i] if i is out of bounds returns None
instead of `IndexOutOfBounds`
Like a regular python list but callable.
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):
"""Allows to use a special syntax for fast-check of `request.args()`
validity
Args:
"""Allows to use a special syntax for fast-check of
`request.args()` validity.
:params:
i: index
default: use this value if arg not found
cast: type cast
otherwise: can be:
- None: results in a 404
- str: redirect to this address
- callable: calls the function (nothing is passed)
otherwise:
will be executed when:
- casts fail
- value not found, dont have default and otherwise is
especified
can be:
- None: results in a 404
- str: redirect to this address
- callable: calls the function (nothing is passed)
Example:
You can use::
request.args(0,default=0,cast=int,otherwise='http://error_url')
request.args(0,default=0,cast=int,otherwise=lambda:...)
"""
n = len(self)
if 0 <= i < n or -n <= i < 0:
@@ -301,23 +303,24 @@ class List(list):
elif default is DEFAULT:
value = None
else:
value, cast = default, False
if cast:
try:
value, cast, otherwise = default, False, False
try:
if cast:
value = cast(value)
except (ValueError, TypeError):
from http import HTTP, redirect
if otherwise is None:
raise HTTP(404)
elif isinstance(otherwise, str):
redirect(otherwise)
elif callable(otherwise):
return otherwise()
else:
raise RuntimeError("invalid otherwise")
if not value and otherwise:
raise ValueError('Otherwise will raised.')
except (ValueError, TypeError):
from http import HTTP, redirect
if otherwise is None:
raise HTTP(404)
elif isinstance(otherwise, str):
redirect(otherwise)
elif callable(otherwise):
return otherwise()
else:
raise RuntimeError("invalid otherwise")
return value
if __name__ == '__main__':
import doctest
doctest.testmod()

View File

@@ -4,6 +4,7 @@ from test_http import *
from test_cache import *
from test_contenttype import *
from test_fileutils import *
from test_globals import *
from test_html import *
from test_is_url import *
from test_languages import *

View File

@@ -26,6 +26,7 @@ exclude_lines =
ignore_errors = True
omit = gluon/contrib/*
gluon/tests/*
gluon/packages/*
[html]
directory = coverage_html_report

View File

@@ -13,6 +13,7 @@ fix_sys_path(__file__)
from storage import Storage
from cache import CacheInRam, CacheOnDisk, Cache
from gluon.dal import DAL, Field
oldcwd = None
@@ -30,6 +31,11 @@ def tearDownModule():
if oldcwd:
os.chdir(oldcwd)
oldcwd = None
try:
os.unlink('dummy.db')
except:
pass
class TestCache(unittest.TestCase):
@@ -107,7 +113,34 @@ class TestCache(unittest.TestCase):
cache.clear(regex=r'a*')
self.assertEqual(cache('a1', lambda: 2, 0), 2)
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__':
setUpModule() # pre-python-2.7

View File

@@ -4,6 +4,7 @@
Unit tests for gluon.dal
"""
import os
import unittest
from fix_path import fix_sys_path
@@ -12,8 +13,15 @@ fix_sys_path(__file__)
from gluon.dal import DAL, Field
def tearDownModule():
try:
os.unlink('dummy.db')
except:
pass
class TestDALSubclass(unittest.TestCase):
def testRun(self):
from gluon.serializers import custom_json, xml
from gluon import sqlhtml
@@ -22,19 +30,26 @@ class TestDALSubclass(unittest.TestCase):
self.assertEqual(db.serializers['xml'], xml)
self.assertEqual(db.representers['rows_render'], sqlhtml.represent)
self.assertEqual(db.representers['rows_xml'], sqlhtml.SQLTABLE)
db.close()
def testSerialization(self):
import pickle
db = DAL(check_reserved=['all'])
db.define_table('t_a', Field('f_a'))
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)
b = pickle.loads(s)
self.assertEqual(a.db, b.db)
db.t_a.drop()
db.close()
""" TODO:
class TestDefaultValidators(unittest.TestCase):
def testRun(self):
pass
"""
if __name__ == '__main__':
unittest.main()
tearDownModule()

124
gluon/tests/test_globals.py Normal file
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()

View File

@@ -120,6 +120,7 @@ class TestStorageList(unittest.TestCase):
class TestList(unittest.TestCase):
""" Tests Storage.List (fast-check for request.args()) """
def test_listcall(self):
@@ -134,6 +135,23 @@ class TestList(unittest.TestCase):
self.assertEqual(a(3, cast=int), 1234)
a.append('x')
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__':

View File

@@ -60,7 +60,7 @@ except ImportError:
# fallback to pure-Python module
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']
### mind there are two loggers here (logger and crud.settings.logger)!
@@ -765,10 +765,13 @@ class Mail(object):
result = {}
try:
if self.settings.server == 'logging':
logger.warn('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))
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)
logger.warn(entry)
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':
xcc = dict()
if cc:
@@ -779,23 +782,23 @@ class Mail(object):
xcc['reply_to'] = reply_to
from google.appengine.api import mail
attachments = attachments and [mail.Attachment(
a.my_filename,
a.my_filename,
a.my_payload,
contebt_id='<attachment-%s>' % k
) for k,a in enumerate(attachments) if not raw]
if attachments:
result = mail.send_mail(
sender=sender, to=origTo,
subject=subject, body=text, html=html,
subject=unicode(subject), body=unicode(text), html=html,
attachments=attachments, **xcc)
elif html and (not raw):
result = mail.send_mail(
sender=sender, to=origTo,
subject=subject, body=text, html=html, **xcc)
subject=unicode(subject), body=unicode(text), html=html, **xcc)
else:
result = mail.send_mail(
sender=sender, to=origTo,
subject=subject, body=text, **xcc)
subject=unicode(subject), body=unicode(text), **xcc)
else:
smtp_args = self.settings.server.split(':')
kwargs = dict(timeout=self.settings.timeout)
@@ -965,7 +968,142 @@ class Recaptcha(DIV):
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):
if style == "divs":
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'),
_class='controls'),
_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:
form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'),
TD(b, _class='w2p_fw'),
@@ -1002,6 +1149,7 @@ class Auth(object):
reset_password_requires_verification=False,
registration_requires_verification=False,
registration_requires_approval=False,
bulk_register_enabled=False,
login_after_registration=False,
login_after_password_change=True,
alternate_requires_registration=False,
@@ -1035,6 +1183,7 @@ class Auth(object):
table_permission_name='auth_permission',
table_event_name='auth_event',
table_cas_name='auth_cas',
table_token_name='auth_token',
table_user=None,
table_group=None,
table_membership=None,
@@ -1103,6 +1252,8 @@ class Auth(object):
retrieve_password_subject='Password retrieve',
reset_password='Click on the link %(link)s to reset your password',
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',
profile_updated='Profile updated',
new_password='New password',
@@ -1316,6 +1467,7 @@ class Auth(object):
settings.update(Auth.default_settings)
settings.update(
cas_domains=[request.env.http_host],
enable_tokens=False,
cas_provider=cas_provider,
cas_actions=dict(login='login',
validate='validate',
@@ -1330,8 +1482,7 @@ class Auth(object):
logged_url=URL(controller, function, args='profile'),
download_url=URL(controller, 'download'),
mailer=(mailer is True) and Mail() or mailer,
on_failed_authorization =
URL(controller, function, args='not_authorized'),
on_failed_authorization = URL(controller, function, args='not_authorized'),
login_next = url_index,
login_onvalidation = [],
login_onaccept = [],
@@ -1356,6 +1507,8 @@ class Auth(object):
change_password_onvalidation = [],
change_password_onaccept = [],
retrieve_password_onvalidation = [],
request_reset_password_onvalidation = [],
request_reset_password_onaccept = [],
reset_password_onvalidation = [],
reset_password_onaccept = [],
hmac_key = hmac_key,
@@ -1391,6 +1544,12 @@ class Auth(object):
next = current.request.vars._next
if isinstance(next, (list, tuple)):
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
def _get_user_id(self):
@@ -1417,6 +1576,9 @@ class Auth(object):
def table_cas(self):
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):
"""
only used in lambda: self._HTTP(404)
@@ -1444,7 +1606,8 @@ class Auth(object):
'retrieve_username', 'retrieve_password',
'reset_password', 'request_reset_password',
'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':
return getattr(self, args[0])(request.args[1])
else:
@@ -1771,7 +1934,7 @@ class Auth(object):
writable=False, readable=False,
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):
"""
To be called unless tables are defined manually
@@ -1798,6 +1961,7 @@ class Auth(object):
username = settings.use_username
else:
settings.use_username = username
settings.enable_tokens = enable_tokens
if not self.signature:
self.define_signature()
if signature == True:
@@ -1981,6 +2145,21 @@ class Auth(object):
migrate=self.__get_migrate(
settings.table_cas_name, 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:
settings.table_user = db[settings.table_user_name]
settings.table_group = db[settings.table_group_name]
@@ -2180,8 +2359,8 @@ class Auth(object):
# user not in database try other login methods
for login_method in self.settings.login_methods:
if login_method != self and login_method(username, password):
self.user = username
return username
self.user = user
return user
return False
def register_bare(self, **fields):
@@ -2190,14 +2369,16 @@ class Auth(object):
and a raw password.
"""
settings = self._get_login_settings()
if not fields.get(settings.passfield):
raise ValueError("register_bare: " +
"password not provided or invalid")
elif not fields.get(settings.userfield):
# users can register_bare even if no password is provided,
# in this case they will have to reset their password to login
if fields.get(settings.passfield):
fields[settings.passfield] = \
settings.table_user[settings.passfield].validate(fields[settings.passfield])[0]
if not fields.get(settings.userfield):
raise ValueError("register_bare: " +
"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, update_fields=self.settings.update_fields)
user = self.get_or_create_user(fields, login=False, get=False,
update_fields=self.settings.update_fields)
if not user:
# get or create did not create a user (it ignores duplicate records)
return False
@@ -2341,10 +2522,6 @@ class Auth(object):
### use session for federated login
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:
session._auth_next = snext
@@ -2717,14 +2894,18 @@ class Auth(object):
passfield = self.settings.password_field
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 = [
Field("password_two", "password", requires=IS_EQUAL_TO(
request.post_vars.get(passfield, None),
error_message=self.messages.mismatched_password),
label=current.T("Confirm Password"))]
else:
extra_fields = []
extra_fields = []
form = SQLFORM(table_user,
fields=self.settings.register_fields,
hidden=dict(_next=next),
@@ -2993,6 +3174,147 @@ class Auth(object):
table_user.email.requires = old_requires
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,
next=DEFAULT,
onvalidation=DEFAULT,
@@ -3030,6 +3352,12 @@ class Auth(object):
except Exception:
session.flash = self.messages.invalid_reset_password
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
form = SQLFORM.factory(
Field('new_password', 'password',
@@ -3045,7 +3373,7 @@ class Auth(object):
formstyle=self.settings.formstyle,
separator=self.settings.label_separator
)
if form.accepts(request, session,
if form.accepts(request, session, onvalidation=onvalidation,
hideerror=self.settings.hideerror):
user.update_record(
**{passfield: str(form.vars.new_password),
@@ -3054,6 +3382,7 @@ class Auth(object):
session.flash = self.messages.password_changed
if self.settings.login_after_password_change:
self.login_user(user)
callback(onaccept, form)
redirect(next, client_side=self.settings.client_side)
return form
@@ -3079,9 +3408,9 @@ class Auth(object):
response.flash = self.messages.function_disabled
return ''
if onvalidation is DEFAULT:
onvalidation = self.settings.reset_password_onvalidation
onvalidation = self.settings.request_reset_password_onvalidation
if onaccept is DEFAULT:
onaccept = self.settings.reset_password_onaccept
onaccept = self.settings.request_reset_password_onaccept
if log is DEFAULT:
log = self.messages['reset_password_log']
userfield = self.settings.login_userfield or 'username' \
@@ -3451,6 +3780,26 @@ class Auth(object):
"""
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):
"""
Decorator that prevents access to action if not logged in or
@@ -3571,7 +3920,7 @@ class Auth(object):
return record.id
else:
id = membership.insert(group_id=group_id, user_id=user_id)
if role:
if role:
self.user_groups[group_id] = role
else:
self.update_groups()
@@ -5174,11 +5523,12 @@ def completion(callback):
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):
dt = datetime.datetime.now() - d
dt = now - d
elif isinstance(d, datetime.date):
dt = datetime.date.today() - d
dt = now.date() - d
elif not d:
return ''
else:
@@ -5362,7 +5712,7 @@ class Expose(object):
if current.request.raw_args:
self.args = [arg for arg in current.request.raw_args.split('/') if arg]
else:
self.args = [arg for arg in current.request.args if args]
self.args = [arg for arg in current.request.args if arg]
filename = os.path.join(base, *self.args)
if not os.path.exists(filename):
raise HTTP(404, "FILE NOT FOUND")
@@ -6049,7 +6399,7 @@ class Wiki(object):
count = db.wiki_tag.wiki_page.count()
fields = [db.wiki_page.id, db.wiki_page.slug,
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:
fields.append(db.wiki_page.body)
if query is None:

View File

@@ -200,8 +200,11 @@ class IS_MATCH(Validator):
self.is_unicode = is_unicode
def __call__(self, value):
if self.is_unicode and not isinstance(value, unicode):
match = self.regex.search(str(value).decode('utf8'))
if self.is_unicode:
if isinstance(value,unicode):
match = self.regex.search(value)
else:
match = self.regex.search(str(value).decode('utf8'))
else:
match = self.regex.search(str(value))
if match is not None:
@@ -3479,7 +3482,7 @@ class IS_IPV6(Validator):
from gluon.contrib import ipaddr as ipaddress
try:
ip = ipaddress.IPv6Address(value)
ip = ipaddress.IPv6Address(value.decode('utf-8'))
ok = True
except ipaddress.AddressValueError:
return (value, translate(self.error_message))
@@ -3491,7 +3494,7 @@ class IS_IPV6(Validator):
self.subnets = [self.subnets]
for network in self.subnets:
try:
ipnet = ipaddress.IPv6Network(network)
ipnet = ipaddress.IPv6Network(network.decode('utf-8'))
except (ipaddress.NetmaskValueError, ipaddress.AddressValueError):
return (value, translate('invalid subnet provided'))
if ip in ipnet:
@@ -3700,20 +3703,22 @@ class IS_IPADDRESS(Validator):
def __call__(self, value):
try:
import ipaddress
from ipaddress import ip_address as IPAddress
from ipaddress import IPv6Address, IPv4Address
except ImportError:
from gluon.contrib import ipaddr as ipaddress
from gluon.contrib.ipaddr import (IPAddress, IPv4Address,
IPv6Address)
try:
ip = ipaddress.IPAddress(value)
except ValueError, e:
ip = IPAddress(value.decode('utf-8'))
except ValueError:
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))
elif self.is_ipv6 and isinstance(ip, ipaddress.IPv4Address):
elif self.is_ipv6 and isinstance(ip, IPv4Address):
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(
minip=self.minip,
maxip=self.maxip,
@@ -3723,7 +3728,7 @@ class IS_IPADDRESS(Validator):
is_automatic=self.is_automatic,
error_message=self.error_message
)(value)
elif self.is_ipv6 or isinstance(ip, ipaddress.IPv6Address):
elif self.is_ipv6 or isinstance(ip, IPv6Address):
retval = IS_IPV6(
is_private=self.is_private,
is_link_local=self.is_link_local,

View File

@@ -1058,6 +1058,11 @@ def start_schedulers(options):
print 'starting single-scheduler for "%s"...' % app_
run(app_, True, True, None, False, code)
return
# Work around OS X problem: http://bugs.python.org/issue9405
import urllib
urllib.getproxies()
for app in apps:
app_, code = get_code_for_scheduler(app, options)
if not app_:

View File

@@ -195,7 +195,7 @@ NameVirtualHost *:80
NameVirtualHost *:443
<VirtualHost *:80>
WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1
WSGIDaemonProcess web2py user=apache group=apache
WSGIProcessGroup web2py
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
WSGIPassAuthorization On

View File

@@ -299,7 +299,7 @@ NameVirtualHost *:80
NameVirtualHost *:443
<VirtualHost *:80>
WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1
WSGIDaemonProcess web2py user=apache group=apache
WSGIProcessGroup web2py
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py

View File

@@ -301,7 +301,7 @@ NameVirtualHost *:80
NameVirtualHost *:443
<VirtualHost *:80>
WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1
WSGIDaemonProcess web2py user=apache group=apache
WSGIProcessGroup web2py
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
WSGIPassAuthorization On

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

View File

@@ -84,7 +84,7 @@ server {
ssl_session_cache shared:SSL: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_protocols SSLv3 TLSv1;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
keepalive_timeout 70;
location / {
#uwsgi_pass 127.0.0.1:9001;

View File

@@ -1,7 +1,7 @@
echo "This script will:
1) install all modules need to run web2py on Ubuntu 14.04
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
5) overwrite /etc/apache2/sites-available/default
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 "================================================="
echo '
WSGIDaemonProcess web2py user=www-data group=www-data processes=1 threads=1
WSGIDaemonProcess web2py user=www-data group=www-data
<VirtualHost *:80>

View File

@@ -0,0 +1,169 @@
"This script will work fine for a few cases 'by default':"
" - completely CLEAN WS2012R2 host"
" - python 2.7 installed in the default path"
" - wfasctgi installed on the default path"
"It'll install web2py under the default website "
" You can use it as a boilerplate to automate your deployments"
" but it still is released AS IT IS. "
"BIG FAT WARNING: It will install a bunch of dependecies
Inspect the source before executing it"
""
""
$ErrorActionPreference = 'stop'
$REALLY_SURE = Read-Host "Do you want to start with web2py deployment? [y/N]"
if (!@('y', 'Y') -contains $REALLY_SURE) {
"Ok, Exiting without doing anything"
exit 1
}
#setting root folder
$rootfolder = $pwd
### utilities - start
function ask_a_question($question) {
$response = Read-Host "$question [Y/n]"
if (@('Y', 'y', '', $null) -contains $response) {
return $true
} else {
return $false
}
}
function unzip_me {
#Load the assembly
[System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null
#Unzip the file
[System.IO.Compression.ZipFile]::ExtractToDirectory($pathToZip, $targetDir)
}
### utilities - end
#install 4.5 that is needed for a bunch of things anyway
Install-WindowsFeature Net-Framework-45-Core
#fetch web2py
$web2py_url = 'http://www.web2py.com/examples/static/web2py_src.zip'
$web2py_file = "$pwd\web2py_src.zip"
if (!(Test-Path $web2py_file)) {
(new-object net.webclient).DownloadFile($web2py_url, $web2py_file)
}
#Load the assembly
[System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null
#Unzip the file
[System.IO.Compression.ZipFile]::ExtractToDirectory($web2py_file, $pwd)
#features installation (IIS, needed modules, python, chocolatey, etc)
$installfeatures = ask_a_question('Do you want to install needed features?')
if ($installfeatures) {
Install-WindowsFeature Web-Server,Web-Default-Doc,Web-Static-Content,Web-Http-Redirect,Web-Http-Logging,Web-Request-Monitor,`
Web-Http-Tracing,Web-Stat-Compression,Web-Dyn-Compression,Web-Filtering,Web-Basic-Auth,Web-Windows-Auth,Web-AppInit,`
Web-CGI,Web-WebSockets,Web-Mgmt-Console,Web-Net-Ext45
}
$copy_web2py = ask_a_question("Copy web2py to the default website root?")
if ($copy_web2py) {
Import-Module WebAdministration
$available_websites = Get-Website
if ($available_websites[0] -eq $null) {
$default_one = $available_websites
} else {
$default_one = $available_websites[0]
}
$iis_root = [System.Environment]::ExpandEnvironmentVariables($default_one.PhysicalPath)
Copy-Item "$rootfolder\web2py\*" $iis_root -Recurse
$rootfolder = $iis_root
$acl = (Get-Item $rootfolder).GetAccessControl('Access')
$identity = "BUILTIN\IIS_IUSRS"
$fileSystemRights = "Modify"
$inheritanceFlags = "ContainerInherit, ObjectInherit"
$propagationFlags = "None"
$accessControlType = "Allow"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, $fileSystemRights, $inheritanceFlags, $propagationFlags, $accessControlType)
$acl.SetAccessRule($rule)
Set-Acl $rootfolder $acl
}
$create_cert = ask_a_question("Do you want to create a self-signed SSL cert?")
if ($create_cert) {
$cert = New-SelfSignedCertificate -DnsName ("localtest.me","*.localtest.me") -CertStoreLocation cert:\LocalMachine\My
$rootStore = Get-Item cert:\LocalMachine\Root
$rootStore.Open("ReadWrite")
$rootStore.Add($cert)
$rootStore.Close();
Import-Module WebAdministration
Set-Location IIS:\SslBindings
New-WebBinding -Name "Default Web Site" -IP "*" -Port 443 -Protocol https
$cert | New-Item 0.0.0.0!443
Set-Location $pwd
}
"checking for chocolatey"
if (Get-Command "choco.exe" -ErrorAction SilentlyContinue)
{
"chocolatey found"
} else {
"installing chocolatey"
(new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1') | iex
}
"installing url-rewrite"
choco install UrlRewrite
$pythonexe = Read-Host 'Python.exe path [C:\Python27\python.exe]'
if (($pythonexe -eq '') -or ($pythonexe -eq $null)) {
$pythonexe = 'C:\Python27\python.exe'
}
if (!(Test-Path $pythonexe)) {
"ERROR: python executable not found"
$pythonwanted = ask_a_question("do you want to install it automatically?")
if ($pythonwanted) {
choco install webpicmd
WebpiCmd.exe /Install /Products:WFastCgi_21_279
$pythonexe = 'C:\Python27\python.exe'
}
else {
exit 1
}
}
$wfastcgipath = Read-Host 'wfastcgi.py path [C:\Python27\Scripts\wfastcgi.py]'
if (($wfastcgipath -eq '') -or ($wfastcgipath -eq $null)) {
$wfastcgipath = 'C:\Python27\Scripts\wfastcgi.py'
}
if (-not (Test-Path $wfastcgipath)) {
"ERROR: wfastcgi.py not found"
$wfastcgiwanted = ask_a_question("do you want to install it automatically?")
if ($wfastcgiwanted) {
choco install webpicmd
WebpiCmd.exe /Install /Products:WFastCgi_21_279
} else {
exit 1
}
}
$pythondir = Split-Path c:\python27\python.exe
#installing dependencies
$env:Path = $env:Path + ";$pythondir;$pythondir\Scripts"
pip install pypiwin32
$PW = Read-Host 'Web2py Admin Password'
$appcmdpath = "$env:windir\system32\inetsrv\appcmd.exe"
& $appcmdpath set config /section:system.webServer/fastCGI "/+[fullPath='$pythonexe', arguments='$wfastcgipath']"
& $appcmdpath unlock config -section:system.webServer/handlers
& cd $rootfolder
& $pythonexe -c "from gluon.main import save_password; save_password('$PW',443)"
$webconfig_template = Join-Path $rootfolder "examples\web.config"
$destination = Join-Path $rootfolder "web.config"
$scriptprocessor = 'scriptProcessor="{0}|{1}"' -f $pythonexe, $wfastcgipath
(Get-Content $webconfig_template) | Foreach-Object {$_ -replace 'scriptProcessor="SCRIPT_PROCESSOR"', $scriptprocessor} | where {$_ -ne ""} | Set-Content $destination
""
"Installation finished. Web2py is available either on http://localhost/ or at https://localtest.me/"
""

78
scripts/tickets2slack.py Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Post error tickets to slack on a 5 minute schedule.
#
# Proper use depends on having created a web-hook through Slack, and having set
# that value in your app's model as the value of global_settings.slack_hook.
# Details on creating web-hooks can be found at https://slack.com/integrations
#
# requires the Requests module for posting to slack, other requirements are
# standard or provided by web2py
#
# Usage (on Unices), replace myapp with the name of your application and run:
# nohup python web2py.py -S myapp -M -R scripts/tickets2slack.py &
import sys
import os
import time
import pickle
import json
try:
import requests
except ImportError as e:
print "missing module 'Requests', aborting."
sys.exit(1)
from gluon import URL
from gluon.utils import md5_hash
from gluon.restricted import RestrictedError
from gluon.settings import global_settings
path = os.path.join(request.folder, 'errors')
sent_errors_file = os.path.join(path, 'slack_errors.pickle')
hashes = {}
if os.path.exists(sent_errors_file):
try:
with open(sent_errors_file, 'rb') as f:
hashes = pickle.load(f)
except Exception as _:
pass
# ## CONFIGURE HERE
SLEEP_MINUTES = 5
ALLOW_DUPLICATES = False
global_settings.slack_hook = global_settings.slack_hook or \
'https://hooks.slack.com/services/your_service'
# ## END CONFIGURATION
while 1:
for file_name in os.listdir(path):
if file_name == 'slack_errors.pickle':
continue
if not ALLOW_DUPLICATES:
key = md5_hash(file_name)
if key in hashes:
continue
hashes[key] = 1
error = RestrictedError()
try:
error.load(request, request.application, file_name)
except Exception as _:
continue # not an exception file?
url = URL(a='admin', f='ticket', args=[request.application, file],
scheme=True)
payload = json.dumps(dict(text="Error in %(app)s.\n%(url)s" %
dict(app=request.application, url=url)))
requests.post(global_settings.slack_hook, data=dict(payload=payload))
with open(sent_errors_file, 'wb') as f:
pickle.dump(hashes, f)
time.sleep(SLEEP_MINUTES * 60)

21
scripts/web2py-scheduler.conf Executable file
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

View File

@@ -62,7 +62,7 @@ do_start()
start-stop-daemon --stop --test --quiet --pidfile $PIDFILE \
&& 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 \
--background --exec $DAEMON -- $DAEMON_ARGS \
|| return 2