Compare commits

..

173 Commits

Author SHA1 Message Date
mdipierro 83681f3f5d fixed markmin test, change beahvior 2017-07-10 03:16:42 -05:00
mdipierro b0a01ef720 fixed some relative imports, thanks steve.van.christen 2017-07-10 03:13:12 -05:00
mdipierro da5543f62e tracking newest pydal 2017-07-10 03:06:08 -05:00
mdipierro dc4ff7c3cc made markmin2html independens on gluon again and fixed reported problem, thanks mweissen 2017-07-10 03:01:35 -05:00
mdipierro 0223b0871e jquery 2.2.4 in admin 2017-07-10 02:43:56 -05:00
mdipierro 89832479fc R-2.16.0b1 2017-07-05 01:48:28 -05:00
mdipierro 15ffdd9a20 R-2.16.0b1 2017-07-05 01:46:01 -05:00
mdipierro 8505c7b282 R-2.16.0b1 2017-07-05 01:42:59 -05:00
mdipierro 5583e9cdc7 R-2.16.0b1 2017-07-05 01:37:48 -05:00
mdipierro 5d8a25626c tracking PyDAL 17.07 2017-07-05 01:32:50 -05:00
mdipierro 9ded289924 fixed minor pylint -E errors 2017-07-02 01:55:51 -05:00
mdipierro f657b42f65 fixed undefined variable 2017-07-02 01:34:05 -05:00
mdipierro 1c0b498880 fixed undefined variable 2017-07-02 01:32:25 -05:00
mdipierro 2fd0c7c778 new dal 2017-06-30 09:22:50 -05:00
mdipierro 3d0b81cbe6 reverted backward imcompatible change 2017-06-30 08:22:26 -05:00
mdipierro 48d69e9724 Merge branch 'master' of github.com:web2py/web2py 2017-06-27 16:43:58 -05:00
mdipierro 757ce4e76e fixes #1528 2017-06-27 16:43:42 -05:00
mdipierro 2f0b429f9e Merge pull request #1651 from BuhtigithuB/improve/pep8-authapi-py
Enhance authapi.py PEP8 compliancy
2017-06-20 16:44:18 -05:00
Richard Vézina a78662e4cc Enhance authapi.py PEP8 compliancy 2017-06-20 16:02:50 -04:00
mdipierro 81fa787ec2 fixes #1514, thanks RekGRpth 2017-06-20 14:59:57 -05:00
mdipierro 159dd0d022 fixed #1542, thanks RekGRpth 2017-06-20 14:49:54 -05:00
mdipierro 16df6840ed fixes #1547, thanks yaminle 2017-06-20 14:47:59 -05:00
mdipierro 0674111129 fixes #1579, thanks Nico 2017-06-20 14:29:47 -05:00
mdipierro 18b755b8da fixed #1583, thanks matclab 2017-06-20 14:24:35 -05:00
mdipierro 2174fc3bec partially fixes #1621, thanks Nico. The strip issue is a python3 error in my opinion 2017-06-20 12:26:14 -05:00
mdipierro 77a947b35c fixed #1638, thanks Anthony 2017-06-20 12:02:40 -05:00
mdipierro 954cef48da fixes #1648, thanks you tbear 2017-06-20 11:53:47 -05:00
mdipierro 566feb79d4 Merge pull request #1649 from ilvalle/fix_globals
fix Pickler.dispatch_table
2017-06-15 04:01:14 -05:00
mdipierro c117e3d1b9 Merge pull request #1644 from leonelcamara/test_is_url_py3
Changed URL validation to use urlparse instead of regex for spliting …
2017-06-15 04:00:30 -05:00
mdipierro 0c3662fb6d scheduler should decode credentials 2017-06-15 03:59:51 -05:00
ilvalle 2ea5939640 fix Pickler.dispatch_table 2017-06-14 21:45:18 +02:00
Leonel Câmara 7c9653ae23 Merge branch 'master' into test_is_url_py3 2017-06-14 15:41:07 +01:00
mdipierro a02538549b Merge pull request #1642 from leonelcamara/restful
Fixes #1634 by adding a parameter "ignore_extension" to Request.restf…
2017-06-14 09:30:52 -05:00
mdipierro ce965cf62a Merge pull request #1641 from leonelcamara/i1639
Fixes #1639
2017-06-14 09:30:25 -05:00
Leonel Câmara 2a33c0faff Changed URL validation to use urlparse instead of regex for spliting the URL
Enabled test_is_url in Python 3 since it is now passing
This might be one of the last fixes to #1353
Fixes #1598
2017-06-07 04:59:03 +01:00
Leonel Câmara ac67beb280 Fixes #1634 by adding a parameter "ignore_extension" to Request.restful which allows you to leave the args untouched if you set it to True. 2017-06-06 01:53:42 +01:00
Leonel Câmara 617ca4a98d Fixes #1639 2017-06-05 23:52:25 +01:00
mdipierro 85ecebc3a4 Merge pull request #1640 from leonelcamara/i1628better
Fixes #1628
2017-06-05 17:44:01 -05:00
Leonel Câmara 376c12a225 Fixes #1628 2017-06-05 23:35:41 +01:00
mdipierro 7769917102 Merge pull request #1637 from timrichardson/issues/1636
fix to #1636: sqlgrid left= (left outer join) fails because db._adapt…
2017-06-05 11:48:33 -05:00
mdipierro 30c28ad44c Merge pull request #1630 from ilvalle/fix_1629
fix web debugger
2017-06-05 11:41:25 -05:00
mdipierro d905968197 Merge pull request #1626 from willimoa/issue/1472
Issue/1472
2017-06-05 11:40:38 -05:00
mdipierro f1ef95e15f Merge pull request #1625 from Scimonster/master
Fix #1624 -- Unicode in XML sanitizing causes error
2017-06-05 11:39:52 -05:00
mdipierro da6688360d Merge pull request #1623 from ilvalle/fix_tag_py3
fix TAG helper on PY3
2017-06-05 11:39:15 -05:00
Tim Richardson f30c31a8a2 fix to #1636: sqlgrid left= (left outer join) fails because db._adapter.tables() now returns dict (previously a table) 2017-06-01 16:53:04 +10:00
Andrew Willimott 59405b9f18 d3 js check was pointing to app, now checking in admin app. 2017-05-16 06:54:51 +12:00
Andrew Willimott 1c747357b0 Repointed appadmin to admin/static for d3 files 2017-05-16 06:47:30 +12:00
Andrew Willimott 6342bf0ddb Consistent set of d3 js and css across admin, examples, and welcome 2017-05-15 20:12:58 +12:00
ilvalle f76437703b fix web debugger, close #1629 2017-05-13 08:26:09 +02:00
Andrew Willimott 6f256be1f1 Added databases variable check 2017-05-08 07:02:31 +12:00
Andrew Willimott 3505e372d8 Remove graphviz graph code from appadmin and design files 2017-05-08 06:40:50 +12:00
Scimonster 49bf14e79a Fix #1624 -- Unicode in XML sanitizing causes error
Add unit test for it
2017-05-07 14:32:52 +03:00
ilvalle cf1ea98217 fix TAG helper on PY3, updated web2pyHTMLParser 2017-05-06 08:51:58 +02:00
mdipierro 8a741023d8 Merge pull request #1622 from willimoa/enhancement/d3_graph
Enhancement/d3 graph
2017-05-03 23:57:42 -05:00
mdipierro 1f4a490a84 Merge pull request #1620 from ilvalle/issue_1618
fix cron with py3, added initial tests, close #1618
2017-05-03 23:57:12 -05:00
Andrew Willimott ca5539561f Typo delete fix for hooks() return 2017-05-04 07:17:04 +12:00
Andrew Willimott 57b554d618 Initial d3 graph commit. Add d3 to graph layout 2017-05-04 07:03:07 +12:00
mdipierro baa129f871 Merge pull request #1527 from leonelcamara/authapi2
Auth refactor
2017-05-01 09:13:18 -05:00
ilvalle 90e606dcfd fix cron with py3, added initial tests, close #1618 2017-05-01 15:31:37 +02:00
mdipierro 1d77968a06 Merge pull request #1617 from ilvalle/issue_1609
fix open file in py3
2017-04-28 08:12:54 -05:00
mdipierro 1842a2e42c Merge pull request #1616 from cccaballero/py3welcome
Fixes in generic layouts for python 3 compatibility
2017-04-28 08:11:42 -05:00
mdipierro c9b6b0faf8 Merge pull request #1607 from sugizo/fix_memcache_appadmin
appadmin can use ccache with cache.ram = cache.disk = cache.memcache
2017-04-28 08:09:17 -05:00
mdipierro d8be963656 Merge pull request #1606 from cccaballero/master
Added on_succes and custom options for web2py ajax function
2017-04-28 08:08:39 -05:00
mdipierro 496112c4fe Merge pull request #1604 from ilvalle/fix_1570
prevent is_empty from stripping whitespaces, close #1570
2017-04-28 08:07:20 -05:00
mdipierro a186f5c51f Merge pull request #1603 from ilvalle/fix_1582_internazionalized_domain_names
fix IS_EMAIL for internationalized domain names
2017-04-28 08:05:30 -05:00
mdipierro fef43cd053 Merge pull request #1602 from leonelcamara/fix_webclientforget
Create a CookieJar on __init__ instead of creating a new one each post
2017-04-28 08:04:56 -05:00
mdipierro ab41cd94ec Merge pull request #1601 from leonelcamara/port_dialog
Make web2pyDialog Python 3 compatible
2017-04-28 08:04:06 -05:00
mdipierro 9ab7ed0029 catching errors in python3 in custom_import 2017-04-28 08:01:20 -05:00
ilvalle 4d117af85f fix open file in py3, close #1609 2017-04-27 18:08:59 +02:00
Carlos Cesar Caballero Díaz 60a6180a77 fixes in generic layouts for python 3 compatibility 2017-04-27 10:35:27 -04:00
sugizo ab537242c4 appadmin can use ccache with cache.ram = cache.disk = cache.memcache 2017-04-13 06:31:40 +07:00
Carlos Cesar Caballero Díaz 02d2fefc21 renamed "on_succes" option to "done" allowing multiple inputs
renamed "on_succes" option to "done" allowing multiple inputs,code
refactoring
2017-04-12 08:48:52 -04:00
Carlos Cesar Caballero Díaz 232598fd8d Added on_succes and custom options for web2py ajax function 2017-04-10 12:23:34 -04:00
ilvalle 7a69e087f7 prevent is_empty from stripping whitespaces, close #1570 2017-04-06 20:48:12 +02:00
ilvalle 6da3a9d8fd fix is_email with internationalized Domain Names, close #1582 2017-04-05 22:16:21 +02:00
Leonel Câmara fe0f506efc Create a CookieJar on __init__ instead of creating a new one each post
Fixes #1505
2017-04-03 19:54:06 +01:00
Leonel Câmara 140023e920 Make web2pyDialog Python 3 compatible
checks "port web2pyDialog" in #1353
2017-03-31 12:04:55 +01:00
mdipierro ad43249f61 Merge branch 'master' of github.com:web2py/web2py 2017-03-21 12:28:14 -05:00
mdipierro dd31fb480c Merge branch 'ilvalle-py36' 2017-03-21 12:27:29 -05:00
mdipierro e5121876db Merge branch 'py36' of https://github.com/ilvalle/web2py into ilvalle-py36 2017-03-21 12:27:11 -05:00
mdipierro 532137bce5 Merge pull request #1594 from BuhtigithuB/improve/translations
Improve french translation and close #1592
2017-03-21 12:24:44 -05:00
mdipierro 35f7caa2f0 Merge pull request #1591 from cccaballero/master
Some fixes on ldap_auth
2017-03-21 12:24:05 -05:00
mdipierro 98dddee697 Merge pull request #1578 from ndegroot/minorcleanupbs3
minor cleanup removed unused class 'navbar-ex1-collapse' see https://…
2017-03-21 12:22:48 -05:00
mdipierro 5000c47472 Merge pull request #1568 from amerikan/patch-2
Remove ! from tag
2017-03-21 12:22:16 -05:00
mdipierro bf3d53ad96 Merge pull request #1567 from amerikan/patch-3
Removed ! from tag
2017-03-21 12:21:13 -05:00
mdipierro b2f221bbaa Merge pull request #1566 from BrenBarn/flexserve
Add FlexibleService, which allows @service-style methods with varargs
2017-03-21 12:18:09 -05:00
mdipierro 868d6f6369 Merge branch 'master' of github.com:web2py/web2py 2017-03-21 12:06:11 -05:00
mdipierro 0e1831bcc7 Merge pull request #1581 from nextghost/pydal-17.01
Update SQLTABLE for API changes in PyDAL 17.01
2017-03-21 12:06:00 -05:00
mdipierro dda808ebda pydal 17.03 2017-03-21 12:05:47 -05:00
mdipierro f31c9002f7 reverted pydal 16.11 2017-03-21 00:42:02 -05:00
mdipierro 6354fbedf8 commented a test 2017-03-21 00:24:17 -05:00
mdipierro cc11a14ce3 commented a test 2017-03-21 00:21:21 -05:00
mdipierro 2fa9597cd8 added debug info 2017-03-21 00:04:55 -05:00
mdipierro c9d656ea45 added logging, thanks gi0baro 2017-03-20 18:06:23 -05:00
mdipierro 1eec01b830 pydal 17.03 2017-03-20 16:21:05 -05:00
mdipierro c10983b191 Merge branch 'master' of github.com:web2py/web2py 2017-03-20 16:15:31 -05:00
mdipierro 975ab7b923 SQLFORM.grid(showblobs=True) 2017-03-20 16:15:18 -05:00
Hardirc a81e116274 Improve french translation and close #1592 2017-03-15 01:03:21 -04:00
Carlos Cesar Caballero Díaz 34f2825a49 fix ldap_auth logging info not shown 2017-03-14 13:09:29 -04:00
Carlos Cesar Caballero Díaz 7dec909254 fixed group mapping on ldap auth 2017-03-14 13:00:32 -04:00
ilvalle ca9198a26e updated to latest dal 2017-03-07 21:11:05 +01:00
ilvalle ad421e42c6 added tests for py36 2017-03-06 21:35:23 +01:00
Martin Doucha 6954988851 Update SQLTABLE for API changes in PyDAL 17.01 2017-02-28 22:12:54 +01:00
Nico de Groot f0382d646c minor cleanup removed unused class 'navbar-ex1-collapse' see https://github.com/twbs/bootstrap/issues/10948 2017-02-26 13:21:44 +01:00
Erik Montes 48a0683dd1 Removed ! from tag 2017-02-07 10:47:26 -08:00
Erik Montes d7fb270e7e Remove ! from tag 2017-02-07 10:46:22 -08:00
BrenBarn 86a2c529b9 Change to modify Service instead of adding FlexibleService 2017-01-31 14:13:43 -08:00
BrenBarn 55592e7c6e Add FlexibleService, which allows @service-style methods that accept varargs 2017-01-31 11:48:28 -08:00
mdipierro 16f60d5cff Merge pull request #1565 from kristjanvalur/master
Update compileapp.py
2017-01-30 12:48:37 -06:00
mdipierro 930370ee91 Merge pull request #1562 from richboss/dev_urandom
don't write to /dev/urandom on Windows platforms
2017-01-30 12:47:50 -06:00
mdipierro 215abc9e4f download over https for security 2017-01-30 12:46:50 -06:00
Kristján Valur Jónsson b6e5e16526 Update compileapp.py
The "current" may have been altered by the controller.  This is the case for admin/controllers/shell.py, in callback(), where a new shell environment is created (from gluon/shell)
If this happens, the incorrect "response" is being referenced and the output of the controller is lost.
2017-01-25 20:10:03 +00:00
Richard Boß b2548f5631 don't write to /dev/urandom on Windows platforms 2017-01-22 21:05:22 +01:00
mdipierro f14c384d83 Merge pull request #1558 from abastardi/fix-router
Fix router functions check
2017-01-19 09:23:16 -06:00
mdipierro 9f186e5d5d Merge pull request #1555 from maxslimmer/master
ccache page fix for disk cache statistics value
2017-01-19 09:23:02 -06:00
mdipierro 51dd427b9e Merge pull request #1550 from senolakkas/patch-2
fix css validation error
2017-01-19 09:22:38 -06:00
abastardi 7b3fe560ed Update test_router_functions to test extensions 2017-01-14 13:14:04 -05:00
abastardi bd526452a8 Fix router functions check
When the router checks whether the requested route matches a function in the
functions list, it does not strip the extension, causing failures for URLs with extensions.
This change strips the extension before comparing to the list of functions.
2017-01-13 15:52:42 -05:00
maxslimmer d862da3543 revert part of fix to displaying cache statistics (#1) 2017-01-04 18:08:57 -08:00
maxslimmer 0165cfadef get cache.disk statistics value for update and display 2017-01-04 18:03:37 -08:00
maxslimmer 46b7716b1d get cache.disk statistics for update 2017-01-04 18:02:23 -08:00
Şenol AKKAŞ 7e5277030a fix css validation error 2016-12-26 14:21:41 +03:00
mdipierro b2e03c9dee Merge pull request #1540 from ilvalle/fix_ipaddress
Replaced ipaddr with ipaddress.py (backported from py3)
2016-12-23 21:58:13 -06:00
mdipierro 0644df5283 Merge pull request #1549 from amerikan/patch-1
typo fix
2016-12-23 21:57:56 -06:00
Erik Montes 4acd9f8f2b typo fix 2016-12-21 11:17:12 -08:00
mdipierro ce0c5f2d5a Merge branch 'master' of github.com:web2py/web2py 2016-12-20 15:49:44 -06:00
mdipierro 28b0385d9b changes to fabfile 2016-12-20 15:49:30 -06:00
mdipierro ac9bccb9a2 Merge pull request #1548 from nextghost/languages
Include Auth and Crud messages in language file updates
2016-12-20 15:42:38 -06:00
mdipierro aa5b40528f Merge pull request #1546 from wish7code/master
Fix for #1443 - PR will enable emperor.uwsgi.service on boot
2016-12-20 15:42:10 -06:00
mdipierro 96efc7bfce Merge pull request #1539 from smorrison/master
when mkdir of missing app folders, handle broken links properly
2016-12-20 15:40:30 -06:00
mdipierro aa584faed6 Merge pull request #1538 from vinyldarkscratch/master
Add web2py componentBegin event
2016-12-20 15:39:55 -06:00
Martin Doucha e7cab3b975 Add Auth and Crud messages when updating language files 2016-12-20 18:54:20 +01:00
wish7 867f93b634 Fix for #1443 - currently emperor.uwsgi.service is not started on boot,
PR will enable emperor.uwsgi.service on boot
(behaviour as in old versions of this script)
2016-12-09 17:21:04 +01:00
mdipierro efff27ffe4 improved fabfile 2016-12-07 23:02:56 -06:00
ilvalle 637579f531 replaced ipaddr with ipaddress.py (backported from py3) 2016-12-03 08:42:35 +01:00
sean 52fe4407b8 when mkdir of missing app folders, handle broken links properly 2016-12-01 10:29:23 -05:00
Vinyl Darkscratch fc0add67b5 Add web2py componentBegin event
Like the “w2p:componentComplete” event I added a while back, this is
another event that I use in my website, and I thought it would be
helpful to share it with others.  This way, you can easily catch when a
web2py component starts loading, and write a handler for whatever
reason (in my case, determining when the user is navigating to a new
page through a smooth loading function piggybacking off of the web2py
component system)
2016-11-29 14:05:17 -08:00
Vinyl Darkscratch 3d2834c81a Merge remote-tracking branch 'web2py/master' 2016-11-29 13:57:46 -08:00
Leonel Câmara a23b264a37 make sudo false again 2016-11-21 19:48:41 +00:00
mdipierro 0b8ecea4dc sudo: required, thanks Leonel 2016-11-21 10:27:16 -06:00
Leonel Câmara 79e256b1d7 require sudo 2016-11-21 15:53:10 +00:00
mdipierro 67945c2d5c Merge pull request #1533 from gchiesa/casv3
implemented base support for CASv3
2016-11-21 09:13:41 -06:00
mdipierro c600854f4b Merge pull request #1532 from vinyldarkscratch/translations
Fix T.M() not adding to language files
2016-11-21 09:12:42 -06:00
Leonel Câmara 920ab72415 fixed _update_session_user not really updating the session 2016-11-20 20:11:45 +00:00
Leonel Câmara 757d46274e fix bug with new login_user 2016-11-20 20:00:51 +00:00
Leonel Câmara 7b66ec0ae3 Fixes #1506 2016-11-20 19:51:51 +00:00
Leonel Câmara bf5ec0d7cf Fixed a long standing bug in login_user which was using 'password' instead of settings.password_field
Fixes #636
2016-11-20 19:38:21 +00:00
Giuseppe Chiesa 2c70a858f1 implemented base support for CASv3 2016-11-17 13:30:11 +01:00
mdipierro ad9ebea900 Merge pull request #1531 from vinyldarkscratch/master
Mobile (iOS) flash dismissal fix
2016-11-14 08:34:00 -06:00
mdipierro c72330f2cd Merge pull request #1530 from BMarvi/master
Update extract_mysql_models.py
2016-11-14 08:33:30 -06:00
mdipierro 091d9c74b0 Merge pull request #1525 from michele-comitini/confirm_registration_redirect_fix
keep the _next while doing the redirect
2016-11-14 08:27:39 -06:00
mdipierro bebdbd9d5e Merge pull request #1519 from matclab/fix/1518
Allow for firstname and lastname in verify_email message
2016-11-14 08:27:10 -06:00
mdipierro 05e28ddffd Merge pull request #1516 from pixelbandito/grammar-uppercase-lowercase
Corrected use of 'lowercase' and 'uppercase' to fix #1515
2016-11-14 08:26:44 -06:00
Vinyl Darkscratch e19435dbcf Prevent Firefox from loading the page "javascript:null;" 2016-11-11 18:51:38 -08:00
Vinyl Darkscratch 90ee6f3754 Add three-quote support on markmin 2016-11-11 15:51:01 -08:00
Vinyl Darkscratch d51ea90e18 Code fix to allow adding markmin translations
Playing with this file made me realize that the quotes defining the
strings themselves are added to the regex searches, and that my
previous addition caused invalid syntax.  So, I decided to fix this by
placing the first quote before the markmin decorator.  (Triple quotes
won’t work, however…)
2016-11-11 15:49:28 -08:00
Vinyl Darkscratch 3fed558bdd Merge remote-tracking branch 'web2py/master' into translations 2016-11-11 15:44:21 -08:00
Vinyl Darkscratch 870d9d3e57 Fix .DS_Store ignore 2016-11-11 13:52:43 -08:00
Vinyl Darkscratch 0a07af55f9 Replace "#" link with "javascript:null;" 2016-11-11 03:31:10 -08:00
Vinyl Darkscratch f7adfbde76 Replace "#" link with "javascript:null;" 2016-11-11 03:30:50 -08:00
Vinyl Darkscratch 75c1d80824 Replace "#" link with "javascript:null;" 2016-11-11 03:30:16 -08:00
Marvi c64a9192d4 Update extract_mysql_models.py 2016-11-11 10:12:11 +01:00
Vinyl Darkscratch 8e79e49ae3 Mobile (iOS) fix for dismissing flashes
iOS devices don’t like listening to clicks on most objects.  They
typically prefer a and button objects.  This fix replaces the
#closeflash span with a link to “#” instead (while also inheriting
text-decoration and color styling), so that mobile (iOS) devices will
allow you to close the flash.
2016-11-10 01:19:06 -08:00
Vinyl Darkscratch 46ce04355f Merge remote-tracking branch 'web2py/master' 2016-11-10 01:14:40 -08:00
Leonel Câmara 85c68e6876 typo 2016-11-05 16:52:42 +00:00
Leonel Câmara d1dfc4a06a use _compat's long 2016-11-05 16:51:54 +00:00
Leonel Câmara 02f0bdb8d3 Auth refactor, extracted many methods into a base class for more generic auth mechanisms.
Partially addresses #1526
Includes a solution for IS_LOWER and IS_UPPER validator problems I mentioned in #1353
2016-11-05 16:37:22 +00:00
Michele Comitini 6b1225da02 keep the _next while doing the redirect 2016-11-04 09:59:49 +01:00
Michele Comitini 4ea31820aa Merge pull request #1520 from michele-comitini/wrong_serializers_import
fix wrong import in default controller of admin app
2016-11-03 12:00:15 +01:00
Michele Comitini 8e1630843a fix wrong import in default controller of admin app 2016-11-02 00:41:40 +01:00
Mathieu Clabaut 2d4817841f Allow for firstname and lastname in verify_email message 2016-11-01 11:31:01 +01:00
Chris Garcia 4226b6d0e1 Corrected use of 'lowercase' and 'uppercase' to fix #1515 2016-10-28 15:23:18 -05:00
Vinyl Darkscratch 3e2b8f89aa Merge remote-tracking branch 'web2py/master' into translations 2016-10-10 20:45:42 -07:00
85 changed files with 6267 additions and 3941 deletions
+1
View File
@@ -13,6 +13,7 @@
*.orig
Thumbs.db
.DS_Store
*.DS_Store
index.yaml
routes.py
logging.conf
+2 -1
View File
@@ -1,6 +1,6 @@
language: python
sudo: false
sudo: required
cache: pip
@@ -8,6 +8,7 @@ python:
- '2.7'
- 'pypy'
- '3.5'
- '3.6'
install:
- pip install -e .
+33
View File
@@ -1,3 +1,36 @@
## 2.16.0b1
- experimental python 3 support
- experimental authapi for service login
- more tests
- d3.js model visulization
- improved scheduler
- is_email support for internationalized Domain Names
- improved used of cookies with CookieJar
- SQLFORM.grid(showblobs=True)
- import JS events (added w2p.componentBegin event)
- added support for CASv3
- allow first_name and last_name placeholders in verify_email message
- added three-quote support in markmin
- updated pg8000 driver (but we still recommend psycopg2)
- compiled views use . separator not _ separator (must recompile code)
- better serbian, french, and catalan translations
- speed improvements (refactor of compileapp and pyc caching)
- removed web shell (never worked as intended)
- allow Expose(..., follow_symlink_out=False).
- Updated fpdf to latest version
- JWT support
- import fabfile for remote deployment
- jQuery 3.2.1
- PyDAL 17.07 including:
allow jsonb support for postgres
correctly configure adapters that need connection for configuration
better caching
updated IMAP adapter methods to new API
experimental suport for joinable subselects
improved Teradata support
improved mongodb support
overall refactoring
experimental support for Google Cloud SQL v2
## 2.15.x
- web2py does not support python 2.6 anymore
+2 -2
View File
@@ -32,7 +32,7 @@ update:
echo "remember that pymysql was tweaked"
src:
### Use semantic versioning
echo 'Version 2.14.6-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
echo 'Version 2.16.0-beta2+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files
make clean
### clean up baisc apps
@@ -54,7 +54,7 @@ src:
### build web2py_src.zip
echo '' > NEWINSTALL
mv web2py_src.zip web2py_src_old.zip | echo 'no old'
cd ..; zip -r web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/gluon/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py
cd ..; zip -r web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/fabfile.py web2py/gluon/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py
mdp:
make src
+1 -1
View File
@@ -1 +1 @@
Version 2.14.6-stable+timestamp.2016.05.09.19.18.48
Version 2.16.0-beta2+timestamp.2017.07.10.03.14.16
+50 -56
View File
@@ -12,11 +12,6 @@ import gluon.contenttype
import gluon.fileutils
from gluon._compat import iteritems
try:
import pygraphviz as pgv
except ImportError:
pgv = None
is_gae = request.env.web2py_runtime_gae or False
# ## critical --- make a copy of the environment
@@ -465,6 +460,7 @@ def ccache():
ram['keys'].append((key, GetInHMS(time.time() - value[0])))
for key in cache.disk.storage:
value = cache.disk.storage[key]
if key == 'web2py_cache_statistics' and isinstance(value[1], dict):
disk['hits'] = value[1]['hit_total'] - value[1]['misses']
disk['misses'] = value[1]['misses']
@@ -564,57 +560,6 @@ def table_template(table):
_cellborder=0, _cellspacing=0)
).xml()
def bg_graph_model():
graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR')
subgraphs = dict()
for tablename in db.tables:
if hasattr(db[tablename],'_meta_graphmodel'):
meta_graphmodel = db[tablename]._meta_graphmodel
else:
meta_graphmodel = dict(group=request.application, color='#ECECEC')
group = meta_graphmodel['group'].replace(' ', '')
if group not in subgraphs:
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename))
for n, key in enumerate(subgraphs.iterkeys()):
graph.subgraph(nbunch=subgraphs[key]['tables'],
name='cluster%d' % n,
style='filled',
color=subgraphs[key]['meta']['color'],
label=subgraphs[key]['meta']['group'])
for tablename in db.tables:
for field in db[tablename]:
f_type = field.type
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
n1 = graph.get_node(tablename)
n2 = graph.get_node(referenced_table)
graph.add_edge(n1, n2, color="#4C4C4C", label='')
graph.layout()
if not request.args:
response.headers['Content-Type'] = 'image/png'
return graph.draw(format='png', prog='dot')
else:
response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0)
if request.args(0) == 'dot':
return graph.string()
else:
return graph.draw(format=request.args(0), prog='dot')
def graph_model():
return dict(databases=databases, pgv=pgv)
def manage():
tables = manager_action['tables']
if isinstance(tables[0], str):
@@ -699,3 +644,52 @@ def hooks():
ul_t.append(UL([LI(A(f['funcname'], _class="editor_filelink", _href=f['url']if 'url' in f else None, **{'_data-lineno':f['lineno']-1})) for f in op['functions']]))
ul_main.append(ul_t)
return ul_main
# ##########################################################
# d3 based model visualizations
# ###########################################################
def d3_graph_model():
""" See https://www.facebook.com/web2py/posts/145613995589010 from Bruno Rocha
and also the app_admin bg_graph_model function
Create a list of table dicts, called "nodes"
"""
data = {}
nodes = []
links = []
subgraphs = dict()
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
links.append(dict(source=tablename, target = referenced_table))
nodes.append(dict(name=tablename, type="table", fields = fields))
# d3 v4 allows individual modules to be specified. The complete d3 library is included below.
response.files.append(URL('admin','static','js/d3.min.js'))
response.files.append(URL('admin','static','js/d3_graph.js'))
return dict(databases=databases, nodes=nodes, links=links)
+2 -1
View File
@@ -6,6 +6,7 @@ import gluon.validators
import code
from gluon.debug import communicate, web_debugger, dbg_debugger
from gluon._compat import thread
from gluon.fileutils import open_file
import pydoc
@@ -54,7 +55,7 @@ def interact():
if filename:
# prevent IOError 2 on some circuntances (EAFP instead of os.access)
try:
lines = open(filename).readlines()
lines = open_file(filename, 'r').readlines()
except:
lines = ""
lines = dict([(i + 1, l) for (i, l) in enumerate(
+5 -2
View File
@@ -16,6 +16,7 @@ from gluon.tools import Config
from gluon.compileapp import find_exposed_functions
from glob import glob
from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native
import gluon.rewrite
import shutil
import platform
@@ -249,6 +250,7 @@ def site():
db.app.insert(name=appname, owner=auth.user.id)
log_progress(appname)
session.flash = T('new application "%s" created', appname)
gluon.rewrite.load()
redirect(URL('design', args=appname))
else:
session.flash = \
@@ -266,6 +268,7 @@ def site():
new_repo = git.Repo.clone_from(form_update.vars.url, target)
session.flash = T('new application "%s" imported',
form_update.vars.name)
gluon.rewrite.load()
except git.GitCommandError as err:
session.flash = T('Invalid git repository specified.')
redirect(URL(r=request))
@@ -302,6 +305,7 @@ def site():
log_progress(appname)
session.flash = T(msg, dict(appname=appname,
digest=md5_hash(installed)))
gluon.rewrite.load()
else:
msg = 'unable to install application "%(appname)s"'
session.flash = T(msg, dict(appname=form_update.vars.name))
@@ -1861,7 +1865,6 @@ def user():
def reload_routes():
""" Reload routes.py """
import gluon.rewrite
gluon.rewrite.load()
redirect(URL('site'))
@@ -1960,7 +1963,7 @@ def git_push():
def plugins():
app = request.args(0)
from serializers import loads_json
from gluon.serializers import loads_json
if not session.plugins:
try:
rawlist = urlopen("http://www.web2pyslices.com/" +
+138 -137
View File
@@ -21,18 +21,19 @@
'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}',
'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'?': '?',
'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
'@markmin\x01Searching: **%s** %%{file}': 'Cherche: **%s** fichiers',
'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'A new version of web2py is available: %s': 'Une nouvelle version de web2py est disponible: %s ',
'A new version of web2py is available: Version 1.68.2 (2009-10-21 09:59:29)\n': 'Une nouvelle version de web2py est disponible: Version 1.68.2 (2009-10-21 09:59:29)\r\n',
'Abort': 'Abort',
'About': 'à propos',
'About application': "A propos de l'application",
'About': 'À propos',
'About application': "À propos de l'application",
'Accept Terms': 'Termes acceptés',
'Add breakpoint': 'Ajouter une interruption',
'additional code for your application': 'code supplémentaire pour votre application',
'Additional code for your application': 'Code additionnel pour votre application',
'Admin design page': 'Admin design page',
'Admin design page': 'Page de conception admin',
'admin disabled because no admin password': 'admin désactivée car aucun mot de passe admin',
'admin disabled because not supported on google app engine': 'admin désactivée car non prise en charge sur Google Apps engine',
'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts',
@@ -41,7 +42,7 @@
'Admin language': "Language de l'admin",
'Admin versioning page': 'Admin versioning page',
'administrative interface': "interface d'administration",
'Administrator Password:': 'Mot de passe Administrateur:',
'Administrator Password:': "Mot de passe de l'administrateur:",
'An error occured, please [[reload %s]] the page': 'Une erreur cest produite, sil vous plait [[reload %s]] la page',
'and rename it (required):': 'et renommez-la (obligatoire):',
'and rename it:': 'et renommez-le:',
@@ -57,14 +58,14 @@
'application is compiled and cannot be designed': "l'application est compilée et ne peut être modifiée",
'Application name:': "Nom de l'application:",
'Application updated via git pull': 'Application updated via git pull',
'are not used': 'are not used',
'are not used yet': 'are not used yet',
'are not used': 'ne sont pas utilisé',
'are not used yet': 'ne sont pas encore utilisé',
'Are you sure you want to delete file "%s"?': 'Êtes-vous sûr de vouloir supprimer le fichier «%s»?',
'Are you sure you want to delete plugin "%s"?': 'Êtes-vous sûr de vouloir supprimer le plugin "%s"?',
'Are you sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?',
'Are you sure you want to uninstall application "%s"?': "Êtes-vous sûr de vouloir désinstaller l'application «%s»?",
'Are you sure you want to upgrade web2py now?': 'Êtes-vous sûr de vouloir mettre à jour web2py maintenant?',
'Are you sure?': 'Etes vous sûr?',
'Are you sure?': 'Êtes vous sûr?',
'arguments': 'arguments',
'at char %s': 'at char %s',
'at line %s': 'at line %s',
@@ -73,13 +74,13 @@
'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: les tests ne sont pas thread-safe DONC NE PAS EFFECTUER DES TESTS MULTIPLES SIMULTANÉMENT.',
'ATTENTION: you cannot edit the running application!': "ATTENTION: vous ne pouvez pas modifier l'application qui tourne!",
'Autocomplete Python Code': 'Autocomplete Python Code',
'Available databases and tables': 'Bases de données et tables disponible',
'Available databases and tables': 'Bases de données et tables disponibles',
'Available Databases and Tables': 'Available Databases and Tables',
'back': 'retour',
'Back to the plugins list': 'Retour à la liste de plugins',
'Back to wizard': 'Back to wizard',
'Basics': 'Basics',
'Begin': 'Begin',
'Begin': 'Début',
'breakpoint': 'breakpoint',
'Breakpoints': 'Breakpoints',
'breakpoints': 'breakpoints',
@@ -92,7 +93,7 @@
'Cache Keys': 'Cache Keys',
'cache, errors and sessions cleaned': 'cache, erreurs et sessions nettoyés',
'can be a git repo': 'can be a git repo',
'Cancel': 'Retour',
'Cancel': 'Annuler',
'Cannot be empty': 'Ne peut pas être vide',
'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Ne peut pas compiler: il y a des erreurs dans votre application. corriger les erreurs et essayez à nouveau.',
'Cannot compile: there are errors in your app:': 'Ne peut pas compiler: il y a des erreurs dans votre application:',
@@ -102,60 +103,60 @@
'Change admin password': 'Changer le mot de passe admin',
'change editor settings': 'change editor settings',
'Changelog': 'Changelog',
'check all': 'tout vérifier ',
'check all': 'tout sélectionner',
'Check for upgrades': 'Vérifier les mises à jour',
'Check to delete': 'Cocher pour supprimer',
'Checking for upgrades...': 'Vérification des mises à jour ... ',
'Clean': 'nettoyer',
'Clear': 'Clear',
'Clear CACHE?': 'Clear CACHE?',
'Clear DISK': 'Clear DISK',
'Clear RAM': 'Clear RAM',
'Clear': 'Effacer',
'Clear CACHE?': 'Effacer le CACHE?',
'Clear DISK': 'Effacer le DISQUE',
'Clear RAM': 'Effacer la RAM',
'Click row to expand traceback': 'Click row to expand traceback',
'Click row to view a ticket': 'Click row to view a ticket',
'click to check for upgrades': 'Cliquez pour vérifier les mises jour',
'code': 'code',
'Code listing': 'Code listing',
'collapse/expand all': 'tout réduire/agrandir',
'Command': 'Command',
'Comment:': 'Comment:',
'Commit': 'Commit',
'Commit form': 'Commit form',
'Committed files': 'Committed files',
'Command': 'Commande',
'Comment:': 'Commentaire:',
'Commit': 'Valider',
'Commit form': 'Valider le formulaire',
'Committed files': 'Fichiers validés',
'Compile': 'compiler',
'Compile (all or nothing)': 'Compile (all or nothing)',
'Compile (skip failed views)': 'Compile (skip failed views)',
'compiled application removed': 'application compilée enlevée',
'Condition': 'Condition',
'continue': 'continue',
'continue': 'continuer',
'Controllers': 'Contrôleurs',
'controllers': 'contrôleurs',
'Count': 'Count',
'Count': 'Compte',
'Create': 'Créer',
'create file with filename:': 'créer un fichier avec nom de fichier:',
'create new application:': 'créer une nouvelle application:',
'Create new simple application': 'Créer une nouvelle application',
'Create/Upload': 'Create/Upload',
'created by': 'créé par',
'Created by:': 'Created by:',
'Created On': 'Created On',
'Created on:': 'Created on:',
'Created by:': 'Créé par:',
'Created On': 'Créé le',
'Created on:': 'Créé le:',
'crontab': 'crontab',
'Current request': 'Requête actuelle',
'Current response': 'Réponse actuelle',
'Current session': 'Session en cours',
'currently running': 'tourne actuellement',
'currently saved or': 'actuellement enregistré ou',
'data uploaded': 'données chargées',
'Database': 'Database',
'data uploaded': 'données téléversées',
'Database': 'Base de données',
'database': 'base de données',
'Database %s select': 'Database %s select',
'database %s select': 'base de données %s sélectionner',
'Database administration': 'Database administration',
'database %s select': 'base de données %s sélectionner',
'Database administration': 'Administration base de données',
'database administration': 'administration base de données',
'Database Administration (appadmin)': 'Database Administration (appadmin)',
'Date and Time': 'Date et heure',
'db': 'bdd',
'db': 'bd',
'Debug': 'Debug',
'defines tables': 'définit les tables',
'Delete': 'Supprimer',
@@ -165,7 +166,7 @@
'Delete this file (you will be asked to confirm deletion)': 'Supprimer ce fichier (on vous demandera de confirmer la suppression)',
'Delete:': 'Supprimer:',
'deleted after first hit': 'deleted after first hit',
'Demo': 'Demo',
'Demo': 'Démo',
'Deploy': 'Déployer',
'Deploy on Google App Engine': 'Déployer sur Google App Engine',
'Deploy to OpenShift': 'Deploy to OpenShift',
@@ -176,22 +177,22 @@
'Description:': 'Description:',
'design': 'conception',
'Detailed traceback description': 'Detailed traceback description',
'details': 'details',
'details': 'détails',
'direction: ltr': 'direction: ltr',
'directory not found': 'directory not found',
'Disable': 'Disable',
'Disabled': 'Disabled',
'Disable': 'Désactiver',
'Disabled': 'Désactivé',
'disabled in demo mode': 'disabled in demo mode',
'disabled in GAE mode': 'disabled in GAE mode',
'disabled in multi user mode': 'disabled in multi user mode',
'DISK': 'DISK',
'DISK': 'DISQUE',
'Disk Cache Keys': 'Disk Cache Keys',
'Disk Cleared': 'Disk Cleared',
'Disk Cleared': 'Disque effacé',
'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Display line numbers': 'Display line numbers',
'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.',
'docs': 'docs',
'Docs': 'Docs',
'docs': 'documents',
'Docs': 'Documents',
'done!': 'fait!',
'Downgrade': 'Downgrade',
'Download .w2p': 'Download .w2p',
@@ -201,43 +202,43 @@
'download plugins': 'télécharger plugins',
'Download plugins from repository': 'Download plugins from repository',
'EDIT': 'MODIFIER',
'Edit': 'modifier',
'edit all': 'edit all',
'Edit': 'Modifier',
'edit all': 'tout modifier',
'Edit application': "Modifier l'application",
'edit controller': 'modifier contrôleur',
'edit controller:': 'edit controller:',
'Edit current record': 'Modifier cette entrée',
'edit views:': 'modifier vues:',
'Editing %s': 'Editing %s',
'edit controller:': 'modifier le contrôleur:',
'Edit current record': 'Modifier cet enregistrement',
'edit views:': 'modifier les vues:',
'Editing %s': 'Modifier %s',
'Editing file': 'Modifier le fichier',
'Editing file "%s"': 'Modifier le fichier "% s" ',
'Editing Language file': 'Modifier le fichier de langue',
'Editing Plural Forms File': 'Editing Plural Forms File',
'Editor': 'Editor',
'Email Address': 'Email Address',
'Enable': 'Enable',
'Editing Plural Forms File': 'Modifier le fichier du formulaire pluriel',
'Editor': 'Éditeur',
'Email Address': 'Adresse courriel',
'Enable': 'Activer',
'Enable Close-Tag': 'Enable Close-Tag',
'Enable Code Folding': 'Enable Code Folding',
'Enterprise Web Framework': 'Enterprise Web Framework',
'Error': 'Error',
'Error logs for "%(app)s"': 'Journal d\'erreurs pour "%(app)s"',
'Error snapshot': 'Error snapshot',
'Error ticket': 'Error ticket',
'Errors': 'erreurs',
'Error ticket': "Billet d'erreur",
'Errors': 'Erreurs',
'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s',
'Exception %s': 'Exception %s',
'Exception instance attributes': "Attributs d'instance Exception",
'Exit Fullscreen': 'Exit Fullscreen',
'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)',
'export as csv file': 'export au format CSV',
'Exports:': 'Exports:',
'Exports:': 'Exportions:',
'exposes': 'expose',
'exposes:': 'expose:',
'extends': 'étend',
'failed to compile file because:': 'failed to compile file because:',
'failed to reload module': 'impossible de recharger le module',
'failed to reload module because:': 'impossible de recharger le module car:',
'File': 'File',
'File': 'Fichier',
'file "%(filename)s" created': 'fichier "%(filename)s" créé',
'file "%(filename)s" deleted': 'fichier "%(filename)s" supprimé',
'file "%(filename)s" uploaded': 'fichier "%(filename)s" chargé',
@@ -247,19 +248,19 @@
'file not found': 'file not found',
'file saved on %(time)s': 'fichier enregistré le %(time)s',
'file saved on %s': 'fichier enregistré le %s',
'filename': 'filename',
'Filename': 'Filename',
'Files added': 'Files added',
'filename': 'nom de fichier',
'Filename': 'Nom de fichier',
'Files added': 'Fichiers ajoutés',
'filter': 'filtre',
'Find Next': 'Find Next',
'Find Previous': 'Find Previous',
'Form has errors': 'Form has errors',
'Find Next': 'Trouver les suivants',
'Find Previous': 'Trouver les précédents',
'Form has errors': 'Le formulaire comporte des erreurs',
'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'Des fonctions sans doctests entraîneront des tests [passed] .',
'GAE Email': 'GAE Email',
'GAE Output': 'GAE Output',
'GAE Password': 'GAE Password',
'Generate': 'Generate',
'GAE Password': 'Mot de passe GAE',
'Generate': 'Générer',
'Git Pull': 'Git Pull',
'Git Push': 'Git Push',
'Globals##debug': 'Globals##debug',
@@ -267,15 +268,15 @@
'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface',
'Google Application Id': 'Google Application Id',
'Goto': 'Goto',
'graph model': 'graph model',
'Graph Model': 'Graph Model',
'graph model': 'représentation graphique du modèle',
'Graph Model': 'Représentation graphique du modèle',
'Help': 'aide',
'here': 'here',
'here': 'ici',
'Hide/Show Translated strings': 'Hide/Show Translated strings',
'Highlight current line': 'Highlight current line',
'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})',
'Hits': 'Hits',
'Home': 'Home',
'Home': 'Accueil',
'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true',
'htmledit': 'edition html',
'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback',
@@ -283,7 +284,7 @@
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.',
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': "Si le rapport ci-dessus contient un numéro de ticket, cela indique une défaillance dans l'exécution du contrôleur, avant toute tentative d'exécuter les doctests. Cela est généralement dû à une erreur d'indentation ou une erreur à l'extérieur du code de la fonction.\r\nUn titre vert indique que tous les tests (si définis) sont passés. Dans ce cas, les résultats des essais ne sont pas affichées.",
'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.',
'import': 'import',
'import': 'importer',
'Import/Export': 'Importer/Exporter',
'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.',
'includes': 'inclus',
@@ -311,14 +312,14 @@
'Invalid request': 'Invalid request',
'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)',
'invalid ticket': 'ticket non valide',
'Key': 'Key',
'Key': 'Clé',
'Keyboard shortcuts': 'Keyboard shortcuts',
'kill process': 'kill process',
'language file "%(filename)s" created/updated': 'fichier de langue "%(filename)s" créé/mis à jour',
'Language files (static strings) updated': 'Fichiers de langue (chaînes statiques) mis à jour ',
'languages': 'langues',
'Languages': 'Langues',
'Last Revision': 'Last Revision',
'Last Revision': 'Dernière révision',
'Last saved on:': 'Dernière sauvegarde le:',
'License for': 'Licence pour',
'License:': 'License:',
@@ -326,8 +327,8 @@
'Line number': 'Line number',
'lists by exception': 'lists by exception',
'lists by ticket': 'lists by ticket',
'Loading...': 'Loading...',
'loading...': 'Chargement ...',
'Loading...': 'Chargement...',
'loading...': 'chargement ...',
'Local Apps': 'Local Apps',
'locals': 'locals',
'Locals##debug': 'Locals##debug',
@@ -337,7 +338,7 @@
'Login to the Administrative Interface': "Se connecter à l'interface d'administration",
'Login/Register': 'Login/Register',
'Logout': 'déconnexion',
'lost password': 'lost password',
'lost password': 'mot de passe perdu',
'Main Menu': 'Main Menu',
'Manage': 'Manage',
'Manage %(action)s': 'Manage %(action)s',
@@ -350,7 +351,7 @@
'merge': 'fusionner',
'Models': 'Modèles',
'models': 'modèles',
'Modified On': 'Modified On',
'Modified On': 'Modifié le',
'Modules': 'Modules',
'modules': 'modules',
'Multi User Mode': 'Multi User Mode',
@@ -363,8 +364,8 @@
'New Record': 'Nouvelle Entrée',
'new record inserted': 'nouvelle entrée insérée',
'New simple application': 'Nouvelle application simple',
'next': 'next',
'next %s rows': 'next %s rows',
'next': 'suivant',
'next %s rows': '%s lignes suivantes',
'next 100 rows': '100 lignes suivantes',
'NO': 'NON',
'no changes': 'no changes',
@@ -374,8 +375,8 @@
'no package selected': 'no package selected',
'no permission to uninstall "%s"': 'no permission to uninstall "%s"',
'Node:': 'Node:',
'Not Authorized': 'Not Authorized',
'Not supported': 'Not supported',
'Not Authorized': 'Pas autori',
'Not supported': 'Pas supporté',
'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.',
'Number of entries: **%s**': 'Number of entries: **%s**',
"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.",
@@ -402,14 +403,14 @@
'Peeking at file': 'Jeter un oeil au fichier',
'Permission': 'Permission',
'Permissions': 'Permissions',
'Please': 'Please',
'Please': 'SVP',
'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...',
'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" supprimé',
'Plugin "%s" in application': 'Plugin "%s" dans l\'application',
'plugin not specified': 'plugin not specified',
'Plugin page': 'Plugin page',
'plugins': 'plugins',
'Plugins': 'Plugins',
'plugins': 'plugiciels',
'Plugins': 'Plugiciels',
'Plural Form #%s': 'Plural Form #%s',
'Plural-Forms:': 'Plural-Forms:',
'Powered by': 'Propulsé par',
@@ -423,7 +424,7 @@
'Pull': 'Pull',
'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.',
'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.',
'Push': 'Push',
'Push': 'Pousser',
'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.',
'pygraphviz library not found': 'pygraphviz library not found',
'PythonAnywhere Apps': 'PythonAnywhere Apps',
@@ -433,9 +434,9 @@
'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram Cleared',
'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Rapid Search': 'Rapid Search',
'Record': 'Record',
'record': 'entrée',
'Rapid Search': 'Recherche rapide',
'Record': 'Enregistrement',
'record': 'enregistrement',
'record does not exist': "l'entrée n'existe pas",
'record id': 'id entrée',
'Record id': 'Record id',
@@ -444,24 +445,24 @@
'Reload routes': 'Reload routes',
'Remove compiled': 'retirer compilé',
'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s',
'Replace': 'Replace',
'Replace All': 'Replace All',
'Replace': 'Remplacer',
'Replace All': 'Tout remplacer',
'Repository (%s)': 'Repository (%s)',
'request': 'request',
'requires distutils, but not installed': 'requires distutils, but not installed',
'requires python-git, but not installed': 'requires python-git, but not installed',
'Resolve Conflict file': 'Résoudre les conflits de fichiers',
'response': 'response',
'restart': 'restart',
'restart': 'redémarrer',
'restore': 'restaurer',
'return': 'return',
'Revert': 'Revert',
'revert': 'revenir',
'return': 'retour',
'Revert': "Revenir vers l'arrière",
'revert': "revenir vers l'arrière",
'reverted to revision %s': 'reverted to revision %s',
'Revision %s': 'Revision %s',
'Revision:': 'Revision:',
'Role': 'Role',
'Roles': 'Roles',
'Revision %s': 'Révision %s',
'Revision:': 'Révision:',
'Role': 'Rôle',
'Roles': 'Rôles',
'Rows in Table': 'Rows in Table',
'Rows in table': 'Lignes de la table',
'Rows selected': 'Lignes sélectionnées',
@@ -470,15 +471,15 @@
'Run tests in this file': 'Run tests in this file',
"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Lancer les tests dans ce fichier (pour lancer tous les fichiers, vous pouvez également utiliser le bouton nommé 'test')",
'Running on %s': 'Running on %s',
'Save': 'Enregistrer',
'save': 'sauver',
'Save file:': 'Save file:',
'Save file: %s': 'Save file: %s',
'Save model as...': 'Save model as...',
'Save via Ajax': 'Save via Ajax',
'Save': 'Sauvegarder',
'save': 'sauvegarder',
'Save file:': 'Sauvegarder le fichier:',
'Save file: %s': 'Sauvegarder le fichier : %s',
'Save model as...': 'Sauvegarder le modèle sous...',
'Save via Ajax': 'Sauvegarder via Ajax',
'Saved file hash:': 'Hash du Fichier enregistré:',
'Screenshot %s': 'Screenshot %s',
'Search': 'Search',
'Screenshot %s': "Capture d'écran %s",
'Search': 'Rechercher',
'Searching: **%s** %%{file}': 'Searching: **%s** %%{file}',
'Select Files to Package': 'Select Files to Package',
'selected': 'sélectionnés',
@@ -491,34 +492,34 @@
'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found',
'Singular Form': 'Singular Form',
'Site': 'Site',
'Size of cache:': 'Size of cache:',
'Size of cache:': 'Taille de la mémoire cache:',
'skip to generate': 'skip to generate',
'some files could not be removed': 'certains fichiers ne peuvent pas être supprimés',
'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying',
'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed',
'source : db': 'source : db',
'source : filesystem': 'source : filesystem',
'Start a new app': 'Start a new app',
'Start searching': 'Start searching',
'source : filesystem': 'source : système de fichier',
'Start a new app': 'Commencer une nouvelle application',
'Start searching': 'Débuté la recherche',
'Start wizard': "Démarrer l'assistant",
'state': 'état',
'Static': 'Static',
'static': 'statiques',
'Static': 'Statique',
'static': 'statique',
'Static files': 'Fichiers statiques',
'Statistics': 'Statistics',
'Step': 'Step',
'step': 'step',
'stop': 'stop',
'stop': 'arrêt',
'submit': 'envoyer',
'Submit': 'Submit',
'successful': 'successful',
'Sure you want to delete this object?': 'Vous êtes sûr de vouloir supprimer cet objet? ',
'switch to : db': 'switch to : db',
'Submit': 'Envoyer',
'successful': 'réussi',
'Sure you want to delete this object?': 'Vous êtes sûr de vouloir supprimer cet objet?',
'switch to : db': 'transférer dans : db',
'switch to : filesystem': 'switch to : filesystem',
'Tab width (# characters)': 'Tab width (# characters)',
'table': 'table',
'Table': 'Table',
'Temporary': 'Temporary',
'Temporary': 'Temporaire',
'test': 'tester',
'Testing application': "Test de l'application",
'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "requête" est une condition comme "db.table1.field1==\'value\'". Quelque chose comme "db.table1.field1==db.table2.field2" aboutit à un JOIN SQL.',
@@ -530,14 +531,14 @@
'The data representation, define database tables and sets': 'La représentation des données, définir les tables et ensembles de la base de données',
'The presentations layer, views are also known as templates': 'Les couches de présentation, les vues sont également appelées modples',
'the presentations layer, views are also known as templates': 'la couche de présentation, les vues sont également appelées modèles',
'Theme': 'Theme',
'Theme': 'Thème',
'There are no controllers': "Il n'y a pas de contrôleurs",
'There are no models': "Il n'y a pas de modèles",
'There are no modules': "Il n'y a pas de modules",
'There are no plugins': "Il n'y a pas de plugins",
'There are no private files': 'There are no private files',
'There are no private files': "Il n'y a pas de fichiers privés",
'There are no static files': "Il n'y a pas de fichiers statiques",
'There are no translators': 'There are no translators',
'There are no translators': "Il n'y a pas de traducteurs",
'There are no translators, only default language is supported': "Il n'y a pas de traducteurs, seule la langue par défaut est prise en charge",
'There are no views': "Il n'y a pas de vues",
'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app',
@@ -552,9 +553,9 @@
'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.',
'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?',
'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".',
'Ticket': 'Ticket',
'Ticket ID': 'Ticket ID',
'Ticket Missing': 'Ticket Missing',
'Ticket': 'Billet',
'Ticket ID': 'Identifiant du Billet',
'Ticket Missing': 'Billet manquant',
'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)',
'TM': 'MD',
'to previous version.': 'à la version précédente.',
@@ -569,8 +570,8 @@
'Translation strings for the application': "Chaînes de traduction pour l'application",
'try': 'essayer',
'try something like': 'essayez quelque chose comme',
'Try the mobile interface': 'Try the mobile interface',
'try view': 'try view',
'Try the mobile interface': "Essayer l'interface pour mobile",
'try view': 'essayer la vue',
'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.',
'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.',
'Unable to check for upgrades': 'Impossible de vérifier les mises à jour',
@@ -595,25 +596,25 @@
'update': 'mettre à jour',
'update all languages': 'mettre à jour toutes les langues',
'Update:': 'Mise à jour:',
'Upgrade': 'Upgrade',
'Upgrade': 'Mese à jour de version',
'upgrade now': 'mettre à jour maintenant',
'upgrade now to %s': 'upgrade now to %s',
'upgrade now to %s': 'mettre à jour maintenant à %s',
'upgrade web2py now': 'mettre à jour web2py maintenant',
'upload': 'charger',
'Upload': 'Upload',
'upload': 'téléversé',
'Upload': 'Téléversé',
'Upload & install packed application': "Charger & installer l'application empaquetée",
'Upload a package:': 'Charger un paquet:',
'Upload and install packed application': 'Upload and install packed application',
'upload application:': "charger l'application:",
'Upload existing application': 'Charger une application existante',
'upload file:': 'charger le fichier:',
'Upload a package:': 'Téléverser un paquet:',
'Upload and install packed application': 'Téléversement et installation du paquet applicatif',
'upload application:': "téléverser l'application:",
'Upload existing application': 'Téléverser une application existante',
'upload file:': 'téléverser le fichier:',
'upload plugin file:': 'charger fichier plugin:',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Utilisez (...)&(...) pour AND, (...)|(...) pour OR, et ~(...) pour NOT afin de construire des requêtes plus complexes. ',
'Use an url:': 'Utiliser une url:',
'user': 'utilisateur',
'User': 'User',
'Username': 'Username',
'Users': 'Users',
'User': 'Utilisateur',
'Username': "Nom d'utilisateur",
'Users': 'Utilisateurs',
'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.',
'variables': 'variables',
'Version': 'Version',
@@ -625,9 +626,9 @@
'Warning!': 'Warning!',
'WARNING:': 'WARNING:',
'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:',
'Web Framework': 'Framework Web',
'web2py Admin Password': 'web2py Admin Password',
'web2py apps to deploy': 'web2py apps to deploy',
'Web Framework': 'Cadre Web',
'web2py Admin Password': 'Mot de passe admin web2py',
'web2py apps to deploy': 'applications web2py à deployer',
'web2py Debugger': 'web2py Debugger',
'web2py downgrade': 'web2py downgrade',
'web2py is up to date': 'web2py est à jour',
@@ -635,10 +636,10 @@
'web2py Recent Tweets': 'Tweets récents sur web2py ',
'web2py upgrade': 'web2py upgrade',
'web2py upgraded; please restart it': 'web2py mis à jour; veuillez le redémarrer',
'Working...': 'Working...',
'Working...': 'Travail en cours...',
'WSGI reference name': 'WSGI reference name',
'YES': 'OUI',
'Yes': 'Yes',
'Yes': 'Oui',
'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button',
'You can inspect variables using the console below': 'You can inspect variables using the console below',
'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out',
+2 -2
View File
@@ -352,9 +352,9 @@
'modules': 'modules',
'Multi User Mode': 'Multi User Mode',
'Must include at least %s %s': 'Moet ten minste bevatten %s %s',
'Must include at least %s lower case': 'Moet ten minste bevatten %s lower case',
'Must include at least %s lowercase': 'Moet ten minste bevatten %s kleine letter',
'Must include at least %s of the following : %s': 'Moet ten minste bevatten %s van het volgende : %s',
'Must include at least %s upper case': 'Moet ten minste bevatten %s upper case',
'Must include at least %s uppercase': 'Moet ten minste bevatten %s hoofdletter',
'new application "%s" created': 'nieuwe applicatie "%s" gemaakt',
'new application "%s" imported': 'new application "%s" imported',
'New Application Wizard': 'Nieuwe Applicatie Wizard',
+2 -2
View File
@@ -356,9 +356,9 @@
'modules': 'модулі',
'Multi User Mode': 'Multi User Mode',
'Must include at least %s %s': 'Має вміщувати щонайменше %s %s',
'Must include at least %s lower case': 'Повинен включати щонайменше %s малих букв',
'Must include at least %s lowercase': 'Повинен включати щонайменше %s малих букв',
'Must include at least %s of the following : %s': 'Має включати не менше %s таких символів : %s',
'Must include at least %s upper case': 'Повинен включати щонайменше %s великих букв',
'Must include at least %s uppercase': 'Повинен включати щонайменше %s великих букв',
'new application "%s" created': 'новий додаток "%s" створено',
'new application "%s" imported': 'new application "%s" imported',
'New Application Wizard': 'Майстер створення нового додатку',
+1 -1
View File
@@ -52,7 +52,7 @@ def verify_password(password):
if DEMO_MODE:
ret = True
elif not _config.get('password'):
ret - False
ret = False
elif _config['password'].startswith('pam_user:'):
session.pam_user = _config['password'][9:].strip()
import gluon.contrib.pam
+3 -3
View File
@@ -314,8 +314,8 @@
// Vim does not support modifier only keys.
return false;
}
// TODO: Current bindings expect the character to be lower case, but
// it looks like vim key notation uses upper case.
// TODO: Current bindings expect the character to be lowercase, but
// it looks like vim key notation uses uppercase.
if (isUpperCase(lastPiece)) {
pieces[pieces.length - 1] = lastPiece.toLowerCase();
}
@@ -3644,7 +3644,7 @@
* Extract the regular expression from the query and return a Regexp object.
* Returns null if the query is blank.
* If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
* If smartCase is passed in, and the query contains upper case letters,
* If smartCase is passed in, and the query contains uppercase letters,
* then ignoreCase is overridden, and the 'i' flag will not be set.
* If the query contains the /i in the flag part of the regular expression,
* then both ignoreCase and smartCase are ignored, and 'i' will be passed
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
.node {fill: steelblue;
stroke: #636363;
stroke-width: 1px;}
.auth {fill: lightgrey;}
.table {r: 10;}
.link {stroke: #bbbbbb;
stroke-width: 2px;}
td {padding: 4px;}
div.tooltip {
position: absolute;
text-align: left;
/* width: 140px; */
/* height: 28px;*/
padding: 0px 5px 0px 5px;
padding-top: 0px;
font: 12px sans-serif;
background: #fff7bc;
border: solid 1px #aaa;
border-radius: 6px;
pointer-events: none;}
h5 { font: 14px sans-serif;
background : #ec7014;
color: #ffffe5;
padding: 5px 2px 5px 2px;
margin-top: 1px;}
path {
fill: #aaaaaa;}
File diff suppressed because one or more lines are too long
+181
View File
@@ -0,0 +1,181 @@
function d3_graph() {
// Some reference links:
// How to get link ids instead of index
// http://stackoverflow.com/questions/23986466/d3-force-layout-linking-nodes-by-name-instead-of-index
// embedding web2py in d3
// http://stackoverflow.com/questions/34326343/embedding-d3-js-graph-in-a-web2py-bootstrap-page
// nodes and links are defined in appadmin.html <script>
var edges = [];
links.forEach(function(e) {
var sourceNode = nodes.filter(function(n) {
return n.name === e.source;
})[0],
targetNode = nodes.filter(function(n) {
return n.name === e.target;
})[0];
edges.push({
source: sourceNode,
target: targetNode,
value: 1});
});
edges.forEach(function(e) {
if (!e.source["linkcount"]) e.source["linkcount"] = 0;
if (!e.target["linkcount"]) e.target["linkcount"] = 0;
e.source["linkcount"]++;
e.target["linkcount"]++;
});
//var width = 960, height = 600;
var height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight;
var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth;
var svg = d3.select("#vis").append("svg")
.attr("width", width)
.attr("height", height);
// updated for d3 v4.
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(strength))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide(35));
// Node charge strength. Repel strength greater for less links.
//function strength(d) { return -50/d["linkcount"] ; }
function strength(d) { return -25 ; }
// Link distance. Distance increases with number of links at source and target
function distance(d) { return (60 + (d.source["linkcount"] * d.target["linkcount"])) ; }
// Link strength. Strength is less for highly connected nodes (move towards target dist)
function strengthl(d) { return 5/(d.source["linkcount"] + d.target["linkcount"]) ; }
simulation
.nodes(nodes)
.on("tick", tick);
simulation.force("link")
.links(edges)
.distance(distance)
.strength(strengthl);
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 25) // Moves the arrow head out, allow for radius
.attr("refY", 0) // -1.5
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var link = svg.selectAll('.link')
.data(edges)
.enter().append('line')
.attr("class", "link")
.attr("marker-end", "url(#end)");
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", function(d) { return "node " + d.type;})
.attr('transform', function(d) {
return "translate(" + d.x + "," + d.y + ")"})
.classed("auth", function(d) { return (d.name.startsWith("auth") ? true : false);});
node.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// add the nodes
node.append('circle')
.attr('r', 16)
;
// add text
node.append("text")
.attr("x", 12)
.attr("dy", "-1.1em")
.text(function(d) {return d.name;});
node.on("mouseover", function(d) {
var g = d3.select(this); // the node (table)
// tooltip
var fields = d.fields;
var fieldformat = "<TABLE>";
fields.forEach(function(d) {
fieldformat += "<TR><TD><B>"+ d.name+"</B></TD><TD>"+ d.type+"</TD><TD>"+ d.disp+"</TD></TR>";
});
fieldformat += "</TABLE>";
var tiplength = d.fields.length;
// Define 'div' for tooltips
var div = d3.select("body").append("div") // declare the tooltip div
.attr("class", "tooltip") // apply the 'tooltip' class
.style("opacity", 0)
.html('<h5>' + d.name + '</h5>' + fieldformat)
.style("left", 20 + (d3.event.pageX) + "px")// or just (d.x + 50 + "px")
.style("top", tooltop(tiplength))// or ...
.transition()
.duration(800)
.style("opacity", 0.9);
});
function tooltop(tiplength) {
//aim to ensure tooltip is fully visible whenver possible
return (Math.max(d3.event.pageY - 20 - (tiplength * 14),0)) + "px"
}
node.on("mouseout", function(d) {
d3.select("body").select('div.tooltip').remove();
});
// instead of waiting for force to end with : force.on('end', function()
// use .on("tick", instead. Here is the tick function
function tick() {
node.attr('transform', function(d) {
d.x = Math.max(30, Math.min(width - 16, d.x));
d.y = Math.max(30, Math.min(height - 16, d.y));
return "translate(" + d.x + "," + d.y + ")"; });
link.attr('x1', function(d) {return d.source.x;})
.attr('y1', function(d) {return d.source.y;})
.attr('x2', function(d) {return d.target.x;})
.attr('y2', function(d) {return d.target.y;});
};
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
};
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
};
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
};
};
File diff suppressed because one or more lines are too long
+43 -11
View File
@@ -38,8 +38,12 @@
if (value > 0) $('#' + id).hide().fadeIn('slow');
else $('#' + id).show().fadeOut('slow');
},
ajax: function (u, s, t) {
ajax: function (u, s, t, options) {
/*simple ajax function*/
// set options default value
options = typeof options !== 'undefined' ? options : {};
var query = '';
if (typeof s == 'string') {
var d = $(s).serialize();
@@ -59,18 +63,44 @@
query = pcs.join('&');
}
}
$.ajax({
// default success action
var success_function = function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
};
// declare success actions as array
var success = [success_function];
// add user success actions
if ($.isArray(options.done)){
success = $.merge(success, options.done);
} else {
success.push(options.done);
}
// default jquery ajax options
var ajax_options = {
type: 'POST',
url: u,
data: query,
success: function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
}
});
success: success
};
//remove custom "done" option if exists
delete options.done;
// merge default ajax options with user custom options
for (var attrname in options) {
ajax_options[attrname] = options[attrname];
}
// call ajax function
$.ajax(ajax_options);
},
ajax_fields: function (target) {
/*
@@ -168,7 +198,8 @@
* and require no dom manipulations
*/
var doc = $(document);
doc.on('click', '.w2p_flash', function () {
doc.on('click', '.w2p_flash', function (event) {
event.preventDefault();
var t = $(this);
if (t.css('top') == '0px') t.slideUp('slow');
else t.fadeOut();
@@ -316,6 +347,7 @@
'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target);
web2py.fire(element, 'w2p:componentBegin', [xhr, settings], target);
return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false
},
'success': function (data, status, xhr) {
+13 -21
View File
@@ -233,30 +233,22 @@
<div class="clear"></div>
{{pass}}
{{if request.function=='graph_model':}}
{{if request.function=='d3_graph_model':}}
<h2>{{=T("Graph Model")}}</h2>
{{if not pgv:}}
{{=T('pygraphviz library not found')}}
{{elif not databases:}}
{{if not databases:}}
{{=T("No databases in this application")}}
{{else:}}
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-download"></i> {{=T('Save model as...')}}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
</ul>
</div>
<br />
{{=IMG(_src=URL('appadmin', 'bg_graph_model'))}}
{{else:}}
<div id="vis"></div>
<link rel="stylesheet" href="{{=URL('static','css/d3_graph.css')}}"/>
<script>
// Define the d3 input data
{{from gluon.serializers import json }}
var nodes = {{=XML(json(nodes))}};
var links = {{=XML(json(links))}};
d3_graph();
</script>
{{pass}}
{{pass}}
{{pass}}
{{if request.function == 'manage':}}
<h2>{{=heading}}</h2>
@@ -34,7 +34,7 @@
<link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.3.1.min.css')}}">
<!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects -->
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script!>
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script>
{{include 'web2py_ajax.html'}}
+3 -1
View File
@@ -105,7 +105,9 @@ def deletefile(arglist, vars={}):
{{if os.access(os.path.join(request.folder,'..',app,'databases','sql.log'),os.R_OK):}}
{{=button(URL('peek/%s/databases/sql.log'%app), 'sql.log')}}
{{pass}}
{{=button(URL(a=app, c='appadmin',f='graph_model'), T('graph model'))}}
{{if os.access(os.path.join(request.folder,'..','admin','static','js','d3_graph.js'),os.R_OK):}}
{{=button(URL(a=app, c='appadmin',f='d3_graph_model'), T('graph model'))}}
{{pass}}
</div>
<ul class="unstyled act_edit">
{{for m in models:}}
+1 -1
View File
@@ -7,7 +7,7 @@ It is used as default when a view is not provided for your controllers
"""}}
<h2>{{=' '.join(x.capitalize() for x in request.function.split('_'))}}</h2>
{{if len(response._vars)==1:}}
{{=BEAUTIFY(response._vars.values()[0])}}
{{=BEAUTIFY(response._vars[next(iter(response._vars))])}}
{{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}}
{{pass}}
@@ -34,7 +34,7 @@
<link rel="stylesheet" href="{{=URL('static','plugin_jqmobile/jquery.mobile-1.3.1.min.css')}}">
<!-- All JavaScript at the bottom, except for Modernizr which enables HTML5 elements & feature detects -->
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script!>
<script src="{{=URL('static','js/modernizr.custom.js')}}"></script>
{{include 'web2py_ajax.html'}}
+50 -56
View File
@@ -12,11 +12,6 @@ import gluon.contenttype
import gluon.fileutils
from gluon._compat import iteritems
try:
import pygraphviz as pgv
except ImportError:
pgv = None
is_gae = request.env.web2py_runtime_gae or False
# ## critical --- make a copy of the environment
@@ -465,6 +460,7 @@ def ccache():
ram['keys'].append((key, GetInHMS(time.time() - value[0])))
for key in cache.disk.storage:
value = cache.disk.storage[key]
if key == 'web2py_cache_statistics' and isinstance(value[1], dict):
disk['hits'] = value[1]['hit_total'] - value[1]['misses']
disk['misses'] = value[1]['misses']
@@ -564,57 +560,6 @@ def table_template(table):
_cellborder=0, _cellspacing=0)
).xml()
def bg_graph_model():
graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR')
subgraphs = dict()
for tablename in db.tables:
if hasattr(db[tablename],'_meta_graphmodel'):
meta_graphmodel = db[tablename]._meta_graphmodel
else:
meta_graphmodel = dict(group=request.application, color='#ECECEC')
group = meta_graphmodel['group'].replace(' ', '')
if group not in subgraphs:
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename))
for n, key in enumerate(subgraphs.iterkeys()):
graph.subgraph(nbunch=subgraphs[key]['tables'],
name='cluster%d' % n,
style='filled',
color=subgraphs[key]['meta']['color'],
label=subgraphs[key]['meta']['group'])
for tablename in db.tables:
for field in db[tablename]:
f_type = field.type
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
n1 = graph.get_node(tablename)
n2 = graph.get_node(referenced_table)
graph.add_edge(n1, n2, color="#4C4C4C", label='')
graph.layout()
if not request.args:
response.headers['Content-Type'] = 'image/png'
return graph.draw(format='png', prog='dot')
else:
response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0)
if request.args(0) == 'dot':
return graph.string()
else:
return graph.draw(format=request.args(0), prog='dot')
def graph_model():
return dict(databases=databases, pgv=pgv)
def manage():
tables = manager_action['tables']
if isinstance(tables[0], str):
@@ -699,3 +644,52 @@ def hooks():
ul_t.append(UL([LI(A(f['funcname'], _class="editor_filelink", _href=f['url']if 'url' in f else None, **{'_data-lineno':f['lineno']-1})) for f in op['functions']]))
ul_main.append(ul_t)
return ul_main
# ##########################################################
# d3 based model visualizations
# ###########################################################
def d3_graph_model():
""" See https://www.facebook.com/web2py/posts/145613995589010 from Bruno Rocha
and also the app_admin bg_graph_model function
Create a list of table dicts, called "nodes"
"""
data = {}
nodes = []
links = []
subgraphs = dict()
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
links.append(dict(source=tablename, target = referenced_table))
nodes.append(dict(name=tablename, type="table", fields = fields))
# d3 v4 allows individual modules to be specified. The complete d3 library is included below.
response.files.append(URL('admin','static','js/d3.min.js'))
response.files.append(URL('admin','static','js/d3_graph.js'))
return dict(databases=databases, nodes=nodes, links=links)
File diff suppressed because one or more lines are too long
+43 -11
View File
@@ -38,8 +38,12 @@
if (value > 0) $('#' + id).hide().fadeIn('slow');
else $('#' + id).show().fadeOut('slow');
},
ajax: function (u, s, t) {
ajax: function (u, s, t, options) {
/*simple ajax function*/
// set options default value
options = typeof options !== 'undefined' ? options : {};
var query = '';
if (typeof s == 'string') {
var d = $(s).serialize();
@@ -59,18 +63,44 @@
query = pcs.join('&');
}
}
$.ajax({
// default success action
var success_function = function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
};
// declare success actions as array
var success = [success_function];
// add user success actions
if ($.isArray(options.done)){
success = $.merge(success, options.done);
} else {
success.push(options.done);
}
// default jquery ajax options
var ajax_options = {
type: 'POST',
url: u,
data: query,
success: function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
}
});
success: success
};
//remove custom "done" option if exists
delete options.done;
// merge default ajax options with user custom options
for (var attrname in options) {
ajax_options[attrname] = options[attrname];
}
// call ajax function
$.ajax(ajax_options);
},
ajax_fields: function (target) {
/*
@@ -168,7 +198,8 @@
* and require no dom manipulations
*/
var doc = $(document);
doc.on('click', '.w2p_flash', function () {
doc.on('click', '.w2p_flash', function (event) {
event.preventDefault();
var t = $(this);
if (t.css('top') == '0px') t.slideUp('slow');
else t.fadeOut();
@@ -316,6 +347,7 @@
'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target);
web2py.fire(element, 'w2p:componentBegin', [xhr, settings], target);
return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false
},
'success': function (data, status, xhr) {
+13 -21
View File
@@ -233,30 +233,22 @@
<div class="clear"></div>
{{pass}}
{{if request.function=='graph_model':}}
{{if request.function=='d3_graph_model':}}
<h2>{{=T("Graph Model")}}</h2>
{{if not pgv:}}
{{=T('pygraphviz library not found')}}
{{elif not databases:}}
{{if not databases:}}
{{=T("No databases in this application")}}
{{else:}}
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-download"></i> {{=T('Save model as...')}}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
</ul>
</div>
<br />
{{=IMG(_src=URL('appadmin', 'bg_graph_model'))}}
{{else:}}
<div id="vis"></div>
<link rel="stylesheet" href="{{=URL('static','css/d3_graph.css')}}"/>
<script>
// Define the d3 input data
{{from gluon.serializers import json }}
var nodes = {{=XML(json(nodes))}};
var links = {{=XML(json(links))}};
d3_graph();
</script>
{{pass}}
{{pass}}
{{pass}}
{{if request.function == 'manage':}}
<h2>{{=heading}}</h2>
@@ -17,10 +17,10 @@
<tbody>
<tr>
<td>
<a class="btn btn180 rounded green" href="http://www.web2py.com/examples/static/web2py_win.zip">For Windows</a>
<a class="btn btn180 rounded green" href="https://mdipierro.pythonanywhere.com/examples/static/web2py_win.zip">For Windows</a>
</td>
<td>
<a class="btn btn180 rounded yellow" href="http://www.web2py.com/examples/static/nightly/web2py_win.zip">For Windows</a>
<a class="btn btn180 rounded yellow" href="https://mdipierro.pythonanywhere.com/examples/static/nightly/web2py_win.zip">For Windows</a>
</td>
<td>
<a class="btn btn180 rounded red" href="http://github.com/web2py/web2py/">Git Repository</a>
@@ -28,19 +28,19 @@
</tr>
<tr>
<td>
<a class="btn btn180 rounded green" href="http://www.web2py.com/examples/static/web2py_osx.zip">For Mac</a>
<a class="btn btn180 rounded green" href="https://mdipierro.pythonanywhere.com/examples/static/web2py_osx.zip">For Mac</a>
</td>
<td>
<a class="btn btn180 rounded yellow" href="http://www.web2py.com/examples/static/nightly/web2py_osx.zip">For Mac</a>
<a class="btn btn180 rounded yellow" href="https://mdipierro.pythonanywhere.com/examples/static/nightly/web2py_osx.zip">For Mac</a>
</td>
<td></td>
</tr>
<tr>
<td>
<a class="btn btn180 rounded green" href="http://www.web2py.com/examples/static/web2py_src.zip">Source Code</a>
<a class="btn btn180 rounded green" href="https://mdipierro.pythonanywhere.com/examples/static/web2py_src.zip">Source Code</a>
</td>
<td>
<a class="btn btn180 rounded yellow" href="http://www.web2py.com/examples/static/nightly/web2py_src.zip">Source Code</a>
<a class="btn btn180 rounded yellow" href="https://mdipierro.pythonanywhere.com/examples/static/nightly/web2py_src.zip">Source Code</a>
</td>
<td>
<a class="btn btn180 rounded red" href="http://web2py.readthedocs.org/en/latest/">Source code docs</a>
+1 -1
View File
@@ -7,7 +7,7 @@ It is used as default when a view is not provided for your controllers
"""}}
<h2>{{=' '.join(x.capitalize() for x in request.function.split('_'))}}</h2>
{{if len(response._vars)==1:}}
{{=BEAUTIFY(response._vars.values()[0])}}
{{=BEAUTIFY(response._vars[next(iter(response._vars))])}}
{{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}}
{{pass}}
+1 -1
View File
@@ -1 +1 @@
{{response.headers['web2py-response-flash']=response.flash}}{{if len(response._vars)==1:}}{{=response._vars.values()[0]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}}
{{response.headers['web2py-response-flash']=response.flash}}{{if len(response._vars)==1:}}{{=response._vars[next(iter(response._vars))]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}}
+50 -56
View File
@@ -12,11 +12,6 @@ import gluon.contenttype
import gluon.fileutils
from gluon._compat import iteritems
try:
import pygraphviz as pgv
except ImportError:
pgv = None
is_gae = request.env.web2py_runtime_gae or False
# ## critical --- make a copy of the environment
@@ -465,6 +460,7 @@ def ccache():
ram['keys'].append((key, GetInHMS(time.time() - value[0])))
for key in cache.disk.storage:
value = cache.disk.storage[key]
if key == 'web2py_cache_statistics' and isinstance(value[1], dict):
disk['hits'] = value[1]['hit_total'] - value[1]['misses']
disk['misses'] = value[1]['misses']
@@ -564,57 +560,6 @@ def table_template(table):
_cellborder=0, _cellspacing=0)
).xml()
def bg_graph_model():
graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR')
subgraphs = dict()
for tablename in db.tables:
if hasattr(db[tablename],'_meta_graphmodel'):
meta_graphmodel = db[tablename]._meta_graphmodel
else:
meta_graphmodel = dict(group=request.application, color='#ECECEC')
group = meta_graphmodel['group'].replace(' ', '')
if group not in subgraphs:
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename))
for n, key in enumerate(subgraphs.iterkeys()):
graph.subgraph(nbunch=subgraphs[key]['tables'],
name='cluster%d' % n,
style='filled',
color=subgraphs[key]['meta']['color'],
label=subgraphs[key]['meta']['group'])
for tablename in db.tables:
for field in db[tablename]:
f_type = field.type
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
n1 = graph.get_node(tablename)
n2 = graph.get_node(referenced_table)
graph.add_edge(n1, n2, color="#4C4C4C", label='')
graph.layout()
if not request.args:
response.headers['Content-Type'] = 'image/png'
return graph.draw(format='png', prog='dot')
else:
response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0)
if request.args(0) == 'dot':
return graph.string()
else:
return graph.draw(format=request.args(0), prog='dot')
def graph_model():
return dict(databases=databases, pgv=pgv)
def manage():
tables = manager_action['tables']
if isinstance(tables[0], str):
@@ -699,3 +644,52 @@ def hooks():
ul_t.append(UL([LI(A(f['funcname'], _class="editor_filelink", _href=f['url']if 'url' in f else None, **{'_data-lineno':f['lineno']-1})) for f in op['functions']]))
ul_main.append(ul_t)
return ul_main
# ##########################################################
# d3 based model visualizations
# ###########################################################
def d3_graph_model():
""" See https://www.facebook.com/web2py/posts/145613995589010 from Bruno Rocha
and also the app_admin bg_graph_model function
Create a list of table dicts, called "nodes"
"""
data = {}
nodes = []
links = []
subgraphs = dict()
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
links.append(dict(source=tablename, target = referenced_table))
nodes.append(dict(name=tablename, type="table", fields = fields))
# d3 v4 allows individual modules to be specified. The complete d3 library is included below.
response.files.append(URL('admin','static','js/d3.min.js'))
response.files.append(URL('admin','static','js/d3_graph.js'))
return dict(databases=databases, nodes=nodes, links=links)
+73 -61
View File
@@ -3,8 +3,8 @@
'!langcode!': 'fr-ca',
'!langname!': 'Français (Canadien)',
'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" est une expression optionnelle comme "champ1=\'nouvellevaleur\'". Vous ne pouvez mettre à jour ou supprimer les résultats d\'un JOIN',
'%s %%{row} deleted': '%s rangées supprimées',
'%s %%{row} updated': '%s rangées mises à jour',
'%s %%{row} deleted': '%s lignes supprimées',
'%s %%{row} updated': '%s lignes mises à jour',
'%s selected': '%s sélectionné',
'%Y-%m-%d': '%Y-%m-%d',
'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S',
@@ -13,12 +13,13 @@
'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}',
'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'?': '?',
'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'about': 'à propos',
'About': 'À propos',
'Access Control': "Contrôle d'accès",
'admin': 'admin',
'Administrative Interface': 'Administrative Interface',
'Administrative Interface': "Interface d'administration",
'Administrative interface': "Interface d'administration",
'Ajax Recipes': 'Recettes Ajax',
'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
@@ -37,37 +38,39 @@
'change password': 'changer le mot de passe',
'Check to delete': 'Cliquez pour supprimer',
'Check to delete:': 'Cliquez pour supprimer:',
'Clear CACHE?': 'Clear CACHE?',
'Clear DISK': 'Clear DISK',
'Clear RAM': 'Clear RAM',
'Clear CACHE?': 'Vider le CACHE?',
'Clear DISK': 'Vider le DISQUE',
'Clear RAM': 'Vider la RAM',
'Client IP': 'IP client',
'Community': 'Communauté',
'Components and Plugins': 'Components and Plugins',
'Components and Plugins': 'Composants et Plugiciels',
'Config.ini': 'Config.ini',
'Controller': 'Contrôleur',
'Copyright': "Droit d'auteur",
'Created By': 'Créé par',
'Created On': 'Créé le',
'Current request': 'Demande actuelle',
'Current response': 'Réponse actuelle',
'Current session': 'Session en cours',
'customize me!': 'personnalisez-moi!',
'data uploaded': 'données téléchargées',
'Database': 'base de données',
'Database %s select': 'base de données %s select',
'Database %s select': 'base de données %s selectionnée',
'Database Administration (appadmin)': 'Database Administration (appadmin)',
'db': 'db',
'DB Model': 'Modèle DB',
'DB Model': 'Modèle BD',
'Delete:': 'Supprimer:',
'Demo': 'Démo',
'Deployment Recipes': 'Recettes de déploiement ',
'Description': 'Descriptif',
'Deployment Recipes': 'Recettes de déploiement',
'Description': 'Description',
'design': 'design',
'Design': 'Design',
'DISK': 'DISK',
'DISK': 'DISQUE',
'Disk Cache Keys': 'Disk Cache Keys',
'Disk Cleared': 'Disk Cleared',
'Disk Cleared': 'Disque vidé',
'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Documentation': 'Documentation',
"Don't know what to do?": "Don't know what to do?",
"Don't know what to do?": 'Vous ne savez pas quoi faire?',
'done!': 'fait!',
'Download': 'Téléchargement',
'E-mail': 'Courriel',
@@ -75,26 +78,27 @@
'Edit current record': "Modifier l'enregistrement courant",
'edit profile': 'modifier le profil',
'Edit This App': 'Modifier cette application',
'Email and SMS': 'Email and SMS',
'Email and SMS': 'Courriel et texto',
'Enter an integer between %(min)g and %(max)g': 'Enter an integer between %(min)g and %(max)g',
'enter an integer between %(min)g and %(max)g': 'entrer un entier compris entre %(min)g et %(max)g',
'Errors': 'Erreurs',
'export as csv file': 'exporter sous forme de fichier csv',
'FAQ': 'faq',
'FAQ': 'FAQ',
'First name': 'Prénom',
'Forms and Validators': 'Formulaires et Validateurs',
'Free Applications': 'Applications gratuites',
'Function disabled': 'Fonction désactivée',
'Graph Model': 'Graph Model',
'Graph Model': 'Représentation graphique du modèle',
'Group %(group_id)s created': '%(group_id)s groupe créé',
'Group ID': 'Groupe ID',
'Group ID': 'ID du groupe',
'Group uniquely assigned to user %(id)s': "Groupe unique attribué à l'utilisateur %(id)s",
'Groups': 'Groupes',
'Hello World': 'Bonjour le monde',
'Helping web2py': 'Helping web2py',
'Helping web2py': 'Aider web2py',
'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})',
'Home': 'Accueil',
'How did you get here?': 'How did you get here?',
'import': 'import',
'import': 'importer',
'Import/Export': 'Importer/Exporter',
'Index': 'Index',
'insert new': 'insérer un nouveau',
@@ -104,40 +108,47 @@
'Invalid email': 'Courriel invalide',
'Invalid Query': 'Requête Invalide',
'invalid request': 'requête invalide',
'Key': 'Key',
'Is Active': 'Est actif',
'Key': 'Clé',
'Last name': 'Nom',
'Layout': 'Mise en page',
'Layout Plugins': 'Layout Plugins',
'Layouts': 'layouts',
'Layout Plugins': 'Plugins de mise en page',
'Layouts': 'Mises en page',
'Live chat': 'Clavardage en direct',
'Live Chat': 'Live Chat',
'Log In': 'Log In',
'Live Chat': 'Clavardage en direct',
'Loading...': 'Chargement...',
'loading...': 'chargement...',
'Log In': 'Connexion',
'Logged in': 'Connecté',
'login': 'connectez-vous',
'Login': 'Connectez-vous',
'logout': 'déconnectez-vous',
'login': 'connexion',
'Login': 'Connexion',
'logout': 'déconnexion',
'lost password': 'mot de passe perdu',
'Lost Password': 'Mot de passe perdu',
'Lost password?': 'Mot de passe perdu?',
'lost password?': 'mot de passe perdu?',
'Main Menu': 'Menu principal',
'Manage %(action)s': 'Manage %(action)s',
'Manage Access Control': 'Manage Access Control',
'Manage Cache': 'Manage Cache',
'Manage Cache': 'Gérer le Cache',
'Memberships': 'Memberships',
'Menu Model': 'Menu modèle',
'My Sites': 'My Sites',
'Modified By': 'Modifié par',
'Modified On': 'Modifié le',
'My Sites': 'Mes sites',
'Name': 'Nom',
'New Record': 'Nouvel enregistrement',
'new record inserted': 'nouvel enregistrement inséré',
'next %s rows': 'next %s rows',
'next %s rows': '%s prochaine lignes',
'next 100 rows': '100 prochaines lignes',
'No databases in this application': "Cette application n'a pas de bases de données",
'Number of entries: **%s**': 'Number of entries: **%s**',
'Object or table name': 'Objet ou nom de table',
'Online book': 'Online book',
'Online examples': 'Exemples en ligne',
'or import from csv file': "ou importer d'un fichier CSV",
'Origin': 'Origine',
'Other Plugins': 'Other Plugins',
'Other Plugins': 'Autres Plugiciels',
'Other Recipes': 'Autres recettes',
'Overview': 'Présentation',
'password': 'mot de passe',
@@ -145,33 +156,34 @@
"Password fields don't match": 'Les mots de passe ne correspondent pas',
'Permission': 'Permission',
'Permissions': 'Permissions',
'please input your password again': "S'il vous plaît entrer votre mot de passe",
'please input your password again': "S'il vous plaît entrer votre mot de passe à nouveau",
'Plugins': 'Plugiciels',
'Powered by': 'Alimenté par',
'Preface': 'Préface',
'previous %s rows': 'previous %s rows',
'previous %s rows': '%s lignes précédentes',
'previous 100 rows': '100 lignes précédentes',
'profile': 'profile',
'pygraphviz library not found': 'pygraphviz library not found',
'profile': 'profil',
'pygraphviz library not found': 'Bibliothèque pygraphviz introuvable',
'Python': 'Python',
'Query:': 'Requête:',
'Quick Examples': 'Examples Rapides',
'Quick Examples': 'Exemples Rapides',
'RAM': 'RAM',
'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram Cleared',
'Ram Cleared': 'Ram vidée',
'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Readme': 'Lisez-moi',
'Recipes': 'Recettes',
'Record': 'enregistrement',
'Record %(id)s created': 'Record %(id)s created',
'Record %(id)s updated': 'Record %(id)s updated',
'Record Created': 'Record Created',
'Record %(id)s created': 'Enregistrement %(id)s créé',
'Record %(id)s updated': 'Enregistrement %(id)s modifié',
'Record Created': 'Enregistrement créé',
'record does not exist': "l'archive n'existe pas",
'Record ID': "ID d'enregistrement",
'Record id': "id d'enregistrement",
'Record Updated': 'Record Updated',
'Record ID': "ID de l'enregistrement",
'Record id': "id de l'enregistrement",
'Record Updated': 'Enregistrement modifié',
'Register': "S'inscrire",
'register': "s'inscrire",
'Registration identifier': "Identifiant d'inscription",
'Registration key': "Clé d'enregistrement",
'Registration successful': 'Inscription réussie',
'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)',
@@ -179,18 +191,18 @@
'Reset Password key': 'Réinitialiser le mot clé',
'Resources': 'Ressources',
'Role': 'Rôle',
'Roles': 'Roles',
'Roles': 'Rôles',
'Rows in Table': 'Lignes du tableau',
'Rows selected': 'Lignes sélectionnées',
'Save model as...': 'Save model as...',
'Save model as...': 'Enregistrer le modèle sous...',
'Semantic': 'Sémantique',
'Services': 'Services',
'Sign Up': 'Sign Up',
'Size of cache:': 'Size of cache:',
'Sign Up': "S'inscrire",
'Size of cache:': 'Taille de la mémoire cache:',
'state': 'état',
'Statistics': 'Statistics',
'Statistics': 'Statistiques',
'Stylesheet': 'Feuille de style',
'submit': 'submit',
'submit': 'soumettre',
'Submit': 'Soumettre',
'Support': 'Soutien',
'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?',
@@ -202,31 +214,31 @@
'The Views': 'Les Vues',
'This App': 'Cette Appli',
'This is a copy of the scaffolding application': "Ceci est une copie de l'application échafaudage",
'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)',
'Time in Cache (h:m:s)': 'Temps en Cache (h:m:s)',
'Timestamp': 'Horodatage',
'Traceback': 'Traceback',
'Twitter': 'Twitter',
'unable to parse csv file': "incapable d'analyser le fichier cvs",
'Update:': 'Mise à jour:',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Employez (...)&(...) pour AND, (...)|(...) pour OR, and ~(...) pour NOT pour construire des requêtes plus complexes.',
'User': 'User',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Employez (...)&(...) pour AND, (...)|(...) pour OR, and ~(...) pour NOT afin de construire des requêtes plus complexes.',
'User': 'Utilisateur',
'User %(id)s Logged-in': 'Utilisateur %(id)s connecté',
'User %(id)s Registered': 'Utilisateur %(id)s enregistré',
'User ID': 'ID utilisateur',
'User Voice': 'User Voice',
'Users': 'Users',
'value already in database or empty': 'valeur déjà dans la base ou vide',
'User Voice': "Voix de l'utilisateur",
'Users': 'Utilisateurs',
'value already in database or empty': 'valeur déjà dans la base ou inexistante',
'Verify Password': 'Vérifiez le mot de passe',
'Videos': 'Vidéos',
'View': 'Présentation',
'View': 'Vue',
'Web2py': 'Web2py',
'Welcome': 'Bienvenu',
'Welcome': 'Bienvenue',
'Welcome %s': 'Bienvenue %s',
'Welcome to web2py': 'Bienvenue à web2py',
'Welcome to web2py!': 'Welcome to web2py!',
'Welcome to web2py!': 'Bienvenue à web2py!',
'Which called the function %s located in the file %s': 'Qui a appelé la fonction %s se trouvant dans le fichier %s',
'Working...': 'Working...',
'You are successfully running web2py': 'Vous roulez avec succès web2py',
'Working...': 'Traitement en cours...',
'You are successfully running web2py': 'Vous exécutez avec succès web2py',
'You can modify this application and adapt it to your needs': "Vous pouvez modifier cette application et l'adapter à vos besoins",
'You visited the url %s': "Vous avez visité l'URL %s",
}
+48 -32
View File
@@ -13,7 +13,9 @@
'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}',
'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'?': '?',
'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)',
'about': 'à propos',
'About': 'À propos',
'Access Control': "Contrôle d'accès",
'admin': 'admin',
@@ -31,7 +33,7 @@
'Cache': 'Cache',
'Cache Cleared': 'Cache Cleared',
'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Cache Keys': 'Clés de cache',
'Cache Keys': 'Cache Keys',
'Cannot be empty': 'Ne peut pas être vide',
'change password': 'changer le mot de passe',
'Check to delete': 'Cliquez pour supprimer',
@@ -41,10 +43,10 @@
'Clear RAM': 'Vider la RAM',
'Client IP': 'IP client',
'Community': 'Communauté',
'Components and Plugins': 'Composants et Plugins',
'Components and Plugins': 'Composants et Plugiciels',
'Config.ini': 'Config.ini',
'Controller': 'Contrôleur',
'Copyright': 'Copyright',
'Copyright': "Droit d'auteur",
'Created By': 'Créé par',
'Created On': 'Créé le',
'Current request': 'Demande actuelle',
@@ -55,8 +57,8 @@
'Database': 'base de données',
'Database %s select': 'base de données %s selectionnée',
'Database Administration (appadmin)': 'Database Administration (appadmin)',
'db': 'bdd',
'DB Model': 'Modèle BDD',
'db': 'db',
'DB Model': 'Modèle BD',
'Delete:': 'Supprimer:',
'Demo': 'Démo',
'Deployment Recipes': 'Recettes de déploiement',
@@ -71,12 +73,13 @@
"Don't know what to do?": 'Vous ne savez pas quoi faire?',
'done!': 'fait!',
'Download': 'Téléchargement',
'E-mail': 'E-mail',
'E-mail': 'Courriel',
'Edit': 'Éditer',
'Edit current record': "Modifier l'enregistrement courant",
'edit profile': 'modifier le profil',
'Edit This App': 'Modifier cette application',
'Email and SMS': 'Email et SMS',
'Email and SMS': 'Courriel et texto',
'Enter an integer between %(min)g and %(max)g': 'Enter an integer between %(min)g and %(max)g',
'enter an integer between %(min)g and %(max)g': 'entrez un entier entre %(min)g et %(max)g',
'Errors': 'Erreurs',
'export as csv file': 'exporter sous forme de fichier csv',
@@ -85,22 +88,24 @@
'Forms and Validators': 'Formulaires et Validateurs',
'Free Applications': 'Applications gratuites',
'Function disabled': 'Fonction désactivée',
'Graph Model': 'Graph Model',
'Group ID': 'Groupe ID',
'Graph Model': 'Représentation graphique du modèle',
'Group %(group_id)s created': '%(group_id)s groupe créé',
'Group ID': 'ID du groupe',
'Group uniquely assigned to user %(id)s': "Groupe unique attribué à l'utilisateur %(id)s",
'Groups': 'Groupes',
'Hello World': 'Bonjour le monde',
'Helping web2py': 'En train daider web2py',
'Helping web2py': 'Aider web2py',
'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})',
'Home': 'Accueil',
'How did you get here?': 'Comment êtes-vous arrivé ici?',
'import': 'Importer',
'How did you get here?': 'How did you get here?',
'import': 'importer',
'Import/Export': 'Importer/Exporter',
'Index': 'Index',
'insert new': 'insérer un nouveau',
'insert new %s': 'insérer un nouveau %s',
'Internal State': 'État interne',
'Introduction': 'Introduction',
'Invalid email': 'E-mail invalide',
'Introduction': 'Présentation',
'Invalid email': 'Courriel invalide',
'Invalid Query': 'Requête Invalide',
'invalid request': 'requête invalide',
'Is Active': 'Est actif',
@@ -109,12 +114,15 @@
'Layout': 'Mise en page',
'Layout Plugins': 'Plugins de mise en page',
'Layouts': 'Mises en page',
'Live chat': 'Chat en direct',
'Live Chat': 'Chat en direct',
'Log In': 'Log In',
'login': 'connectez-vous',
'Login': 'Connectez-vous',
'logout': 'déconnectez-vous',
'Live chat': 'Clavardage en direct',
'Live Chat': 'Clavardage en direct',
'Loading...': 'Chargement...',
'loading...': 'chargement...',
'Log In': 'Connexion',
'Logged in': 'Connecté',
'login': 'connexion',
'Login': 'Connexion',
'logout': 'déconnexion',
'lost password': 'mot de passe perdu',
'Lost Password': 'Mot de passe perdu',
'Lost password?': 'Mot de passe perdu?',
@@ -131,7 +139,7 @@
'Name': 'Nom',
'New Record': 'Nouvel enregistrement',
'new record inserted': 'nouvel enregistrement inséré',
'next %s rows': 'next %s rows',
'next %s rows': '%s prochaine lignes',
'next 100 rows': '100 prochaines lignes',
'No databases in this application': "Cette application n'a pas de bases de données",
'Number of entries: **%s**': 'Number of entries: **%s**',
@@ -140,55 +148,63 @@
'Online examples': 'Exemples en ligne',
'or import from csv file': "ou importer d'un fichier CSV",
'Origin': 'Origine',
'Other Plugins': 'Autres Plugins',
'Other Plugins': 'Autres Plugiciels',
'Other Recipes': 'Autres recettes',
'Overview': 'Présentation',
'password': 'mot de passe',
'Password': 'Mot de passe',
"Password fields don't match": 'Les mots de passe ne correspondent pas',
'Permission': 'Permission',
'Permissions': 'Permissions',
'Plugins': 'Plugins',
'please input your password again': "S'il vous plaît entrer votre mot de passe à nouveau",
'Plugins': 'Plugiciels',
'Powered by': 'Alimenté par',
'Preface': 'Préface',
'previous %s rows': 'previous %s rows',
'previous %s rows': '%s lignes précédentes',
'previous 100 rows': '100 lignes précédentes',
'profile': 'profil',
'pygraphviz library not found': 'Bibliothèque pygraphviz introuvable',
'Python': 'Python',
'Query:': 'Requête:',
'Quick Examples': 'Exemples Rapides',
'RAM': 'RAM',
'RAM Cache Keys': 'Clés de cache de la RAM',
'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram vidée',
'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
'Readme': 'Lisez-moi',
'Recipes': 'Recettes',
'Record': 'enregistrement',
'Record %(id)s created': 'Enregistrement %(id)s créé',
'Record %(id)s updated': 'Enregistrement %(id)s modifié',
'Record Created': 'Enregistrement créé',
'record does not exist': "l'archive n'existe pas",
'Record ID': "ID d'enregistrement",
'Record id': "id d'enregistrement",
'Record ID': "ID de l'enregistrement",
'Record id': "id de l'enregistrement",
'Record Updated': 'Enregistrement modifié',
'Register': "S'inscrire",
'register': "s'inscrire",
'Registration identifier': "Identifiant d'enregistrement",
'Registration identifier': "Identifiant d'inscription",
'Registration key': "Clé d'enregistrement",
'Registration successful': 'Inscription réussie',
'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)',
'Request reset password': 'Demande de réinitialiser le mot clé',
'Reset Password key': 'Réinitialiser le mot clé',
'Resources': 'Ressources',
'Role': 'Rôle',
'Roles': 'Roles',
'Roles': 'Rôles',
'Rows in Table': 'Lignes du tableau',
'Rows selected': 'Lignes sélectionnées',
'Save model as...': 'Enregistrer le modèle sous...',
'Semantic': 'Sémantique',
'Services': 'Services',
'Sign Up': 'Sign Up',
'Size of cache:': 'Taille du cache:',
'Sign Up': "S'inscrire",
'Size of cache:': 'Taille de la mémoire cache:',
'state': 'état',
'Statistics': 'Statistiques',
'Stylesheet': 'Feuille de style',
'submit': 'soumettre',
'Submit': 'Soumettre',
'Support': 'Support',
'Support': 'Soutien',
'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?',
'Table': 'tableau',
'Table name': 'Nom du tableau',
+2 -2
View File
@@ -20,8 +20,8 @@
# YOU CAN COPY THIS FILE TO ANY APPLICATION'S ROOT DIRECTORY WITHOUT CHANGES!
# ----------------------------------------------------------------------------------------------------------------------
from fileutils import abspath
from languages import read_possible_languages
from gluon.fileutils import abspath
from gluon.languages import read_possible_languages
possible_languages = read_possible_languages(abspath('applications', app))
# ----------------------------------------------------------------------------------------------------------------------
File diff suppressed because one or more lines are too long
+43 -11
View File
@@ -38,8 +38,12 @@
if (value > 0) $('#' + id).hide().fadeIn('slow');
else $('#' + id).show().fadeOut('slow');
},
ajax: function (u, s, t) {
ajax: function (u, s, t, options) {
/*simple ajax function*/
// set options default value
options = typeof options !== 'undefined' ? options : {};
var query = '';
if (typeof s == 'string') {
var d = $(s).serialize();
@@ -59,18 +63,44 @@
query = pcs.join('&');
}
}
$.ajax({
// default success action
var success_function = function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
};
// declare success actions as array
var success = [success_function];
// add user success actions
if ($.isArray(options.done)){
success = $.merge(success, options.done);
} else {
success.push(options.done);
}
// default jquery ajax options
var ajax_options = {
type: 'POST',
url: u,
data: query,
success: function (msg) {
if (t) {
if (t == ':eval') eval(msg);
else if (typeof t == 'string') $('#' + t).html(msg);
else t(msg);
}
}
});
success: success
};
//remove custom "done" option if exists
delete options.done;
// merge default ajax options with user custom options
for (var attrname in options) {
ajax_options[attrname] = options[attrname];
}
// call ajax function
$.ajax(ajax_options);
},
ajax_fields: function (target) {
/*
@@ -168,7 +198,8 @@
* and require no dom manipulations
*/
var doc = $(document);
doc.on('click', '.w2p_flash', function () {
doc.on('click', '.w2p_flash', function (event) {
event.preventDefault();
var t = $(this);
if (t.css('top') == '0px') t.slideUp('slow');
else t.fadeOut();
@@ -316,6 +347,7 @@
'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target);
web2py.fire(element, 'w2p:componentBegin', [xhr, settings], target);
return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false
},
'success': function (data, status, xhr) {
+13 -21
View File
@@ -233,30 +233,22 @@
<div class="clear"></div>
{{pass}}
{{if request.function=='graph_model':}}
{{if request.function=='d3_graph_model':}}
<h2>{{=T("Graph Model")}}</h2>
{{if not pgv:}}
{{=T('pygraphviz library not found')}}
{{elif not databases:}}
{{if not databases:}}
{{=T("No databases in this application")}}
{{else:}}
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-download"></i> {{=T('Save model as...')}}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
</ul>
</div>
<br />
{{=IMG(_src=URL('appadmin', 'bg_graph_model'))}}
{{else:}}
<div id="vis"></div>
<link rel="stylesheet" href="{{=URL('static','css/d3_graph.css')}}"/>
<script>
// Define the d3 input data
{{from gluon.serializers import json }}
var nodes = {{=XML(json(nodes))}};
var links = {{=XML(json(links))}};
d3_graph();
</script>
{{pass}}
{{pass}}
{{pass}}
{{if request.function == 'manage':}}
<h2>{{=heading}}</h2>
+1 -1
View File
@@ -7,7 +7,7 @@ It is used as default when a view is not provided for your controllers
"""}}
<h2>{{=' '.join(x.capitalize() for x in request.function.split('_'))}}</h2>
{{if len(response._vars)==1:}}
{{=BEAUTIFY(response._vars.values()[0])}}
{{=BEAUTIFY(response._vars[next(iter(response._vars))])}}
{{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}}
{{pass}}
+2 -2
View File
@@ -7,7 +7,7 @@
# it is not safe to use as a generic.jsonp because of security implications.
if response.view == 'generic.jsonp':
raise HTTP(501,'generic.jsonp diasbled for security reasons')
raise HTTP(501,'generic.jsonp disabled for security reasons')
try:
from gluon.serializers import json
@@ -20,4 +20,4 @@ except ImportError:
raise HTTP(405, 'JSON not available')
except:
raise HTTP(405, 'JSON error')
}}
}}
+1 -1
View File
@@ -27,4 +27,4 @@ Notice:
- no need to return a string
even if the function is called via ajax.
'''}}{{if len(response._vars)==1:}}{{=response._vars.values()[0]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}}
'''}}{{if len(response._vars)==1:}}{{=response._vars[next(iter(response._vars))]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}}
+1 -1
View File
@@ -59,7 +59,7 @@
</button>
{{=response.logo or ''}}
</div>
<div class="collapse navbar-collapse navbar-ex1-collapse">
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
{{='auth' in globals() and auth.navbar('Welcome',mode='dropdown') or ''}}
</ul>
Vendored
+3 -2
View File
@@ -1,7 +1,8 @@
from fabric.api import *
from fabric.operations import put, get
from fabric.contrib.files import exists
from fabric.contrib.files import exists, append, uncomment
import os
import crypt
import datetime
import getpass
@@ -19,11 +20,11 @@ def create_user(username):
"""fab -H root@host create_user:username"""
password = getpass.getpass('password for %s> ' % username)
run('useradd -m -G www-data -s /bin/bash -p %s %s' % (crypt.crypt(password, 'salt'), username))
local('ssh-copy-id %s' % env.hosts[0])
run('cp /etc/sudoers /tmp/sudoers.new')
append('/tmp/sudoers.new', '%s ALL=(ALL) NOPASSWD:ALL' % username, use_sudo=True)
run('visudo -c -f /tmp/sudoers.new')
run('EDITOR="cp /tmp/sudoers.new" visudo')
local('ssh-copy-id %s' % env.hosts[0])
uncomment('~%s/.bashrc' % username, '#force_color_prompt=yes')
def install_web2py():
+2
View File
@@ -32,6 +32,7 @@ if PY2:
import cgi
import cookielib
from xmlrpclib import ProtocolError
from gluon.contrib import ipaddress
BytesIO = StringIO
reduce = reduce
hashlib_md5 = hashlib.md5
@@ -97,6 +98,7 @@ else:
from http import cookiejar as cookielib
from xmlrpc.client import ProtocolError
import html # warning, this is the python3 module and not the web2py html module
import ipaddress
hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8'))
iterkeys = lambda d: iter(d.keys())
itervalues = lambda d: iter(d.values())
+5 -1
View File
@@ -438,7 +438,11 @@ def add_path_first(path):
def try_mkdir(path):
if not os.path.exists(path):
try:
os.mkdir(path)
if os.path.islink(path):
# path is a broken link, try to mkdir the target of the link instead of the link itself.
os.mkdir(os.path.realpath(path))
else:
os.mkdir(path)
except OSError as e:
if e.strerror == 'File exists': # In case of race condition.
pass
+1038
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -637,10 +637,10 @@ def run_controller_in(controller, function, environment):
web2py_error=badf)
code = "%s\nresponse._vars=response._caller(%s)" % (code, function)
layer = "%s:%s" % (filename, function)
ccode = getcfs(layer, filename, lambda: compile2(code, layer))
ccode = getcfs(layer, filename, lambda: compile2(code, filename))
restricted(ccode, environment, layer=filename)
response = current.response
response = environment["response"]
vars = response._vars
if response.postprocessing:
vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -506,9 +506,9 @@ def ldap_auth(server='ldap',
l = []
for group in ldap_groups_of_the_user:
if group in group_mapping:
l += group_mapping[group]
l.append(group_mapping[group])
ldap_groups_of_the_user = l
logging.info("User groups after remapping: %s" % str(l))
logger.info("User groups after remapping: %s" % str(l))
#
# Get all group name where the user is in actually in local db
@@ -528,7 +528,7 @@ def ldap_auth(server='ldap',
except AttributeError as e:
db_user_id = db.auth_user.insert(email=username, first_name=username)
if not db_user_id:
logging.error(
logger.error(
'There is no username or email for %s!' % username)
raise
# if old pydal version, assume this is a relational database which can do joins
@@ -550,7 +550,7 @@ def ldap_auth(server='ldap',
for group in db_group_search.select(db.auth_group.id, db.auth_group.role, distinct=True):
db_group_id[group.role] = group.id
db_groups_of_the_user.append(group.role)
logging.debug('db groups of user %s: %s' % (username, str(db_groups_of_the_user)))
logger.debug('db groups of user %s: %s' % (username, str(db_groups_of_the_user)))
auth_membership_changed = False
#
+57 -26
View File
@@ -5,12 +5,19 @@
# license MIT/BSD/GPL
from __future__ import print_function
import re
import sys
import urllib
from gluon._compat import maketrans, urllib_quote, unicodeT, to_bytes, to_native, xrange
from gluon.utils import local_html_escape as escape
from ast import parse as ast_parse
import ast
PY2 = sys.version_info[0] == 2
if PY2:
from urllib import quote as urllib_quote
from string import maketrans
else:
from urllib.parse import quote as urllib_quote
maketrans = str.maketrans
"""
TODO: next version should use MathJax
@@ -544,7 +551,7 @@ regex_code = re.compile(
'(' + META + '|' + DISABLED_META + r'|````)|(``(?P<t>.+?)``(?::(?P<c>[a-zA-Z][_a-zA-Z\-\d]*)(?:\[(?P<p>[^\]]*)\])?)?)',
re.S)
regex_strong = re.compile(r'\*\*(?P<t>[^\s*]+( +[^\s*]+)*)\*\*')
regex_del = re.compile(r'~~(?P<t>[^\s*]+( +[^\s*]+)*)~~')
regex_del = re.compile(r'~~(?P<t>[^\s~]+( +[^\s~]+)*)~~')
regex_em = re.compile(r"''(?P<t>([^\s']| |'(?!'))+)''")
regex_num = re.compile(r"^\s*[+-]?((\d+(\.\d*)?)|\.\d+)([eE][+-]?[0-9]+)?\s*$")
regex_list = re.compile('^(?:(?:(#{1,6})|(?:(\.+|\++|\-+)(\.)?))\s*)?(.*)$')
@@ -564,6 +571,29 @@ ttab_in = maketrans("'`:*~\\[]{}@$+-.#\n", '\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14
ttab_out = maketrans('\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x05', "'`:*~\\[]{}@$+-.#\n")
regex_quote = re.compile('(?P<name>\w+?)\s*\=\s*')
def local_html_escape(data, quote=False):
"""
Works with bytes.
Replace special characters "&", "<" and ">" to HTML-safe sequences.
If the optional flag quote is true (the default), the quotation mark
characters, both double quote (") and single quote (') characters are also
translated.
"""
if PY2:
import cgi
data = cgi.escape(data, quote)
return data.replace("'", "&#x27;") if quote else data
else:
import html
if isinstance(data, str):
return html.escape(data, quote=quote)
data = data.replace(b"&", b"&amp;") # Must be done first!
data = data.replace(b"<", b"&lt;")
data = data.replace(b">", b"&gt;")
if quote:
data = data.replace(b'"', b"&quot;")
data = data.replace(b'\'', b"&#x27;")
return data
def make_dict(b):
return '{%s}' % regex_quote.sub("'\g<name>':", b)
@@ -579,7 +609,7 @@ def safe_eval(node_or_string, env):
_safe_names = {'None': None, 'True': True, 'False': False}
_safe_names.update(env)
if isinstance(node_or_string, basestring):
node_or_string = ast_parse(node_or_string, mode='eval')
node_or_string = ast.parse(node_or_string, mode='eval')
if isinstance(node_or_string, ast.Expression):
node_or_string = node_or_string.body
@@ -599,14 +629,14 @@ def safe_eval(node_or_string, env):
if node.id in _safe_names:
return _safe_names[node.id]
elif isinstance(node, ast.BinOp) and \
isinstance(node.op, (Add, Sub)) and \
isinstance(node.right, Num) and \
isinstance(node.op, (ast.Add, ast.Sub)) and \
isinstance(node.right, ast.Num) and \
isinstance(node.right.n, complex) and \
isinstance(node.left, Num) and \
isinstance(node.left, ast.Num) and \
isinstance(node.left.n, (int, long, float)):
left = node.left.n
right = node.right.n
if isinstance(node.op, Add):
if isinstance(node.op, ast.Add):
return left + right
else:
return left - right
@@ -765,7 +795,7 @@ def render(text,
'<table><tbody><tr class="first"><td>a</td><td>b</td></tr><tr class="even"><td>c</td><td>d</td></tr></tbody></table>'
>>> render("----\\nhello world\\n----\\n")
'<blockquote>hello world</blockquote>'
'<blockquote><p>hello world</p></blockquote>'
>>> render('[[myanchor]]')
'<p><span class="anchor" id="markmin_myanchor"></span></p>'
@@ -946,7 +976,8 @@ def render(text,
if protolinks == "default":
protolinks = protolinks_simple
pp = '\n' if pretty_print else ''
text = to_native(text)
text = text if text is None or isinstance(text, str) else text.decode('utf8', 'strict')
if not (isinstance(text, str)):
text = str(text or '')
text = regex_backslash.sub(lambda m: m.group(1).translate(ttab_in), text)
@@ -994,7 +1025,7 @@ def render(text,
return LINK
text = regex_link.sub(mark_link, text)
text = escape(text)
text = local_html_escape(text)
if protolinks:
text = regex_proto.sub(lambda m: protolinks(*m.group('p', 'k')), text)
@@ -1035,7 +1066,7 @@ def render(text,
if pend and mtag == '.': # paragraph in a list:
out.append(etags.pop())
ltags.pop()
for i in xrange(lent - lev):
for i in range(lent - lev):
out.append('<' + tag + '>' + pp)
etags.append('</' + tag + '>' + pp)
lev += 1
@@ -1044,7 +1075,7 @@ def render(text,
elif lent == lev:
if tlev[-1] != tag:
# type of list is changed (ul<=>ol):
for i in xrange(ltags.count(lent)):
for i in range(ltags.count(lent)):
ltags.pop()
out.append(etags.pop())
tlev[-1] = tag
@@ -1209,7 +1240,7 @@ def render(text,
s = '<blockquote%s%s>%s</blockquote>%s' \
% (t_cls,
t_id,
'\n'.join(strings[bq_begin:lineno]), pp)
render('\n'.join(strings[bq_begin:lineno])), pp)
mtag = 'q'
else:
s = '<hr />'
@@ -1322,10 +1353,10 @@ def render(text,
t, a, k, p, w = m.group('t', 'a', 'k', 'p', 'w')
if not k:
return m.group(0)
k = escape(k)
k = local_html_escape(k)
t = t or ''
style = 'width:%s' % w if w else ''
title = ' title="%s"' % escape(a).replace(META, DISABLED_META) if a else ''
title = ' title="%s"' % local_html_escape(a).replace(META, DISABLED_META) if a else ''
p_begin = p_end = ''
if p == 'center':
p_begin = '<p style="text-align:center">'
@@ -1349,7 +1380,7 @@ def render(text,
autolinks, protolinks, class_prefix, id_prefix, pretty_print)
return '<%(p)s controls="controls"%(title)s%(style)s><source src="%(k)s" />%(t)s</%(p)s>' \
% dict(p=p, title=title, style=style, k=k, t=t)
alt = ' alt="%s"' % escape(t).replace(META, DISABLED_META) if t else ''
alt = ' alt="%s"' % local_html_escape(t).replace(META, DISABLED_META) if t else ''
return '%(begin)s<img src="%(k)s"%(alt)s%(title)s%(style)s />%(end)s' \
% dict(begin=p_begin, k=k, alt=alt, title=title, style=style, end=p_end)
@@ -1358,12 +1389,12 @@ def render(text,
if not k and not t:
return m.group(0)
t = t or ''
a = escape(a) if a else ''
a = local_html_escape(a) if a else ''
if k:
if '#' in k and ':' not in k.split('#')[0]:
# wikipage, not external url
k = k.replace('#', '#' + id_prefix)
k = escape(k)
k = local_html_escape(k)
title = ' title="%s"' % a.replace(META, DISABLED_META) if a else ''
target = ' target="_blank"' if p == 'popup' else ''
t = render(t, {}, {}, 'br', URL, environment, latex, None,
@@ -1373,7 +1404,7 @@ def render(text,
if t == 'NEWLINE' and not a:
return '<br />' + pp
return '<span class="anchor" id="%s">%s</span>' % (
escape(id_prefix + t),
local_html_escape(id_prefix + t),
render(a, {}, {}, 'br', URL,
environment, latex, autolinks,
protolinks, class_prefix,
@@ -1399,7 +1430,7 @@ def render(text,
def expand_meta(m):
code, b, p, s = segments.pop(0)
if code is None or m.group() == DISABLED_META:
return escape(s)
return local_html_escape(s)
if b in extra:
if code[:1] == '\n':
code = code[1:]
@@ -1411,7 +1442,7 @@ def render(text,
return str(extra[b](code))
elif b == 'cite':
return '[' + ','.join('<a href="#%s" class="%s">%s</a>' %
(id_prefix + d, b, d) for d in escape(code).split(',')) + ']'
(id_prefix + d, b, d) for d in local_html_escape(code).split(',')) + ']'
elif b == 'latex':
return LATEX % urllib_quote(code)
elif b in html_colors:
@@ -1426,12 +1457,12 @@ def render(text,
% (fg, bg, render(code, {}, {}, 'br', URL, environment, latex,
autolinks, protolinks, class_prefix, id_prefix, pretty_print))
cls = ' class="%s%s"' % (class_prefix, b) if b and b != 'id' else ''
id = ' id="%s%s"' % (id_prefix, escape(p)) if p else ''
id = ' id="%s%s"' % (id_prefix, local_html_escape(p)) if p else ''
beg = (code[:1] == '\n')
end = [None, -1][code[-1:] == '\n']
if beg and end:
return '<pre><code%s%s>%s</code></pre>%s' % (cls, id, escape(code[1:-1]), pp)
return '<code%s%s>%s</code>' % (cls, id, escape(code[beg:end]))
return '<pre><code%s%s>%s</code></pre>%s' % (cls, id, local_html_escape(code[1:-1]), pp)
return '<code%s%s>%s</code>' % (cls, id, local_html_escape(code[beg:end]))
text = regex_expand_meta.sub(expand_meta, text)
+3
View File
@@ -21,6 +21,9 @@ def MemcacheClient(*a, **b):
class MemcacheClientObj(Client):
def initialize(self):
pass
meta_storage = {}
max_time_expire = 24*3600
+3 -3
View File
@@ -105,14 +105,14 @@ class ServerProxy(object):
def __getattr__(self, attr):
"pseudo method that can be called"
return lambda *args: self.call(attr, *args)
return lambda *args, **vars: self.call(attr, *args, **vars)
def call(self, method, *args):
def call(self, method, *args, **vars):
"JSON RPC communication (method invocation)"
# build data sent to the service
request_id = random.randint(0, sys.maxsize)
data = {'id': request_id, 'method': method, 'params': args, }
data = {'id': request_id, 'method': method, 'params': args or vars, }
if self.version:
data['jsonrpc'] = self.version #mandatory key/value for jsonrpc2 validation else err -32600
request = json.dumps(data)
+2 -2
View File
@@ -43,6 +43,7 @@ class WebClient(object):
self.forms = {}
self.history = []
self.cookies = {}
self.cookiejar = cookielib.CookieJar()
self.default_headers = default_headers
self.sessions = {}
self.session_regex = session_regex and re.compile(session_regex)
@@ -79,9 +80,8 @@ class WebClient(object):
cookies = cookies or {}
headers = headers or {}
cj = cookielib.CookieJar()
args = [
urllib2.HTTPCookieProcessor(cj),
urllib2.HTTPCookieProcessor(self.cookiejar),
urllib2.HTTPHandler(debuglevel=0)
]
# if required do basic auth
+2 -2
View File
@@ -75,7 +75,7 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1):
try:
oname = name if not name.startswith('.') else '.'+name
return NATIVE_IMPORTER(oname, globals, locals, fromlist, level)
except ImportError:
except (ImportError, KeyError):
items = current.request.folder.split(os.path.sep)
if not items[-1]:
items = items[:-1]
@@ -100,7 +100,7 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1):
import_tb = sys.exc_info()[2]
try:
return NATIVE_IMPORTER(name, globals, locals, fromlist, level)
except ImportError as e3:
except (ImportError, KeyError) as e3:
raise ImportError(e1, import_tb) # there an import error in the module
except Exception as e2:
raise # there is an error in the module
+13 -13
View File
@@ -89,7 +89,7 @@ def communicate(command=None):
# New debugger implementation using dbg and a web UI
import gluon.contrib.dbg as dbg
import gluon.contrib.dbg as c_dbg
from threading import RLock
interact_lock = RLock()
@@ -109,11 +109,11 @@ def check_interaction(fn):
return check_fn
class WebDebugger(dbg.Frontend):
class WebDebugger(c_dbg.Frontend):
"""Qdb web2py interface"""
def __init__(self, pipe, completekey='tab', stdin=None, stdout=None):
dbg.Frontend.__init__(self, pipe)
c_dbg.Frontend.__init__(self, pipe)
self.clear_interaction()
def clear_interaction(self):
@@ -128,7 +128,7 @@ class WebDebugger(dbg.Frontend):
run_lock.acquire()
try:
while self.pipe.poll():
dbg.Frontend.run(self)
c_dbg.Frontend.run(self)
finally:
run_lock.release()
@@ -149,23 +149,23 @@ class WebDebugger(dbg.Frontend):
@check_interaction
def do_continue(self):
dbg.Frontend.do_continue(self)
c_dbg.Frontend.do_continue(self)
@check_interaction
def do_step(self):
dbg.Frontend.do_step(self)
c_dbg.Frontend.do_step(self)
@check_interaction
def do_return(self):
dbg.Frontend.do_return(self)
c_dbg.Frontend.do_return(self)
@check_interaction
def do_next(self):
dbg.Frontend.do_next(self)
c_dbg.Frontend.do_next(self)
@check_interaction
def do_quit(self):
dbg.Frontend.do_quit(self)
c_dbg.Frontend.do_quit(self)
def do_exec(self, statement):
interact_lock.acquire()
@@ -175,18 +175,18 @@ class WebDebugger(dbg.Frontend):
# avoid spurious interaction notifications:
self.set_burst(2)
# execute the statement in the remote debugger:
return dbg.Frontend.do_exec(self, statement)
return c_dbg.Frontend.do_exec(self, statement)
finally:
interact_lock.release()
# create the connection between threads:
parent_queue, child_queue = Queue.Queue(), Queue.Queue()
front_conn = dbg.QueuePipe("parent", parent_queue, child_queue)
child_conn = dbg.QueuePipe("child", child_queue, parent_queue)
front_conn = c_dbg.QueuePipe("parent", parent_queue, child_queue)
child_conn = c_dbg.QueuePipe("child", child_queue, parent_queue)
web_debugger = WebDebugger(front_conn) # frontend
dbg_debugger = dbg.Qdb(pipe=child_conn, redirect_stdio=False, skip=None) # backend
dbg_debugger = c_dbg.Qdb(pipe=child_conn, redirect_stdio=False, skip=None) # backend
dbg = dbg_debugger
# enable getting context (stack, globals/locals) at interaction
+2 -2
View File
@@ -9,7 +9,7 @@ Based on http://code.activestate.com/recipes/52257/
Licensed under the PSF License
"""
from gluon._compat import to_unicode
import codecs
# None represents a potentially variable byte. "##" in the XML spec...
@@ -77,4 +77,4 @@ def autoDetectXMLEncoding(buffer):
def decoder(buffer):
encoding = autoDetectXMLEncoding(buffer)
return buffer.decode(encoding).encode('utf8')
return to_unicode(buffer, charset=encoding)
+5 -5
View File
@@ -89,10 +89,11 @@ class SortingPickler(Pickler):
self._batch_setitems([(key, obj[key]) for key in sorted(obj)])
if PY2:
#FIXME PY3
SortingPickler.dispatch = copy.copy(Pickler.dispatch)
SortingPickler.dispatch[dict] = SortingPickler.save_dict
else:
SortingPickler.dispatch_table = copyreg.dispatch_table.copy()
SortingPickler.dispatch_table[dict] = SortingPickler.save_dict
def sorting_dumps(obj, protocol=None):
file = StringIO()
@@ -349,14 +350,14 @@ class Request(Storage):
current.session.forget()
redirect(URL(scheme='https', args=self.args, vars=self.vars))
def restful(self):
def restful(self, ignore_extension=False):
def wrapper(action, request=self):
def f(_action=action, *a, **b):
request.is_restful = True
env = request.env
is_json = env.content_type=='application/json'
method = env.request_method
if len(request.args) and '.' in request.args[-1]:
if not ignore_extension and len(request.args) and '.' in request.args[-1]:
request.args[-1], _, request.extension = request.args[-1].rpartition('.')
current.response.headers['Content-Type'] = \
contenttype('.' + request.extension.lower())
@@ -415,7 +416,6 @@ class Response(Storage):
if not escape:
self.body.write(str(data))
else:
# FIXME PY3:
self.body.write(to_native(xmlescape(data)))
def render(self, *a, **b):
+13 -27
View File
@@ -20,7 +20,7 @@ import urllib
import base64
from gluon import sanitizer, decoder
import itertools
from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool, text_type
from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool, text_type, long
from gluon.utils import local_html_escape
import marshal
@@ -596,10 +596,10 @@ class XML(XmlComponent):
for A, IMG and BlockQuote).
The key is the tag; the value is a list of allowed attributes.
"""
if sanitize:
text = sanitizer.sanitize(text, permitted_tags, allowed_attributes)
if isinstance(text, unicodeT):
text = to_native(text.encode('utf8', 'xmlcharrefreplace'))
if sanitize:
text = sanitizer.sanitize(text, permitted_tags, allowed_attributes)
elif isinstance(text, bytes):
text = to_native(text)
elif not isinstance(text, str):
@@ -998,9 +998,9 @@ class DIV(XmlComponent):
if isinstance(c, XmlComponent):
s = c.flatten(render)
elif render:
s = render(str(c))
s = render(to_native(c))
else:
s = str(c)
s = to_native(c)
text += s
if render:
text = render(text, self.tag, self.attributes)
@@ -1281,7 +1281,6 @@ class __TAG__(XmlComponent):
def __getattr__(self, name):
if name[-1:] == '_':
name = name[:-1] + '/'
name=to_bytes(name)
return lambda *a, **b: __tag_div__(name, *a, **b)
def __call__(self, html):
@@ -2376,17 +2375,17 @@ class FORM(DIV):
def as_json(self, sanitize=True):
d = self.as_dict(flat=True, sanitize=sanitize)
from serializers import json
from gluon.serializers import json
return json(d)
def as_yaml(self, sanitize=True):
d = self.as_dict(flat=True, sanitize=sanitize)
from serializers import yaml
from gluon.serializers import yaml
return yaml(d)
def as_xml(self, sanitize=True):
d = self.as_dict(flat=True, sanitize=sanitize)
from serializers import xml
from gluon.serializers import xml
return xml(d)
@@ -2655,36 +2654,24 @@ class web2pyHTMLParser(HTMLParser):
"""
obj = web2pyHTMLParser(text) parses and html/xml text into web2py helpers.
obj.tree contains the root of the tree, and tree can be manipulated
>>> str(web2pyHTMLParser('hello<div a="b" c=3>wor&lt;ld<span>xxx</span>y<script/>yy</div>zzz').tree)
'hello<div a="b" c="3">wor&lt;ld<span>xxx</span>y<script></script>yy</div>zzz'
>>> str(web2pyHTMLParser('<div>a<span>b</div>c').tree)
'<div>a<span>b</span></div>c'
>>> tree = web2pyHTMLParser('hello<div a="b">world</div>').tree
>>> tree.element(_a='b')['_c']=5
>>> str(tree)
'hello<div a="b" c="5">world</div>'
"""
def __init__(self, text, closed=('input', 'link')):
HTMLParser.__init__(self)
self.tree = self.parent = TAG['']()
self.closed = closed
self.tags = [x for x in __all__ if isinstance(eval(x), DIV)]
self.last = None
self.feed(text)
def handle_starttag(self, tagname, attrs):
if tagname.upper() in self.tags:
tag = eval(tagname.upper())
else:
if tagname in self.closed:
tagname += '/'
tag = TAG[tagname]()
if tagname in self.closed:
tagname += '/'
tag = TAG[tagname]()
for key, value in attrs:
tag['_' + key] = value
tag.parent = self.parent
self.parent.append(tag)
if not tag.tag.endswith(b'/'):
if not tag.tag.endswith('/'):
self.parent = tag
else:
self.last = tag.tag[:-1]
@@ -2707,7 +2694,6 @@ class web2pyHTMLParser(HTMLParser):
self.parent.append(entitydefs[name])
def handle_endtag(self, tagname):
tagname = to_bytes(tagname)
# this deals with unbalanced tags
if tagname == self.last:
return
+18 -10
View File
@@ -951,32 +951,40 @@ def findT(path, language=DEFAULT_LANGUAGE):
Note:
Must be run by the admin app
"""
from gluon.tools import Auth, Crud
lang_file = pjoin(path, 'languages', language + '.py')
sentences = read_dict(lang_file)
mp = pjoin(path, 'models')
cp = pjoin(path, 'controllers')
vp = pjoin(path, 'views')
mop = pjoin(path, 'modules')
def add_message(message):
if not message.startswith('#') and not '\n' in message:
tokens = message.rsplit('##', 1)
else:
# this allows markmin syntax in translations
tokens = [message]
if len(tokens) == 2:
message = tokens[0].strip() + '##' + tokens[1].strip()
if message and not message in sentences:
sentences[message] = message.replace("@markmin\x01", "")
for filename in \
listdir(mp, '^.+\.py$', 0) + listdir(cp, '^.+\.py$', 0)\
+ listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0):
data = to_native(read_locked(filename))
items = regex_translate.findall(data)
items += ["@markmin\x01%s" %x for x in regex_translate_m.findall(data)]
for x in regex_translate_m.findall(data):
if x[0:3] in ["'''", '"""']: items.append("%s@markmin\x01%s" %(x[0:3], x[3:]))
else: items.append("%s@markmin\x01%s" %(x[0], x[1:]))
for item in items:
try:
message = safe_eval(item)
except:
continue # silently ignore inproperly formatted strings
if not message.startswith('#') and not '\n' in message:
tokens = message.rsplit('##', 1)
else:
# this allows markmin syntax in translations
tokens = [message]
if len(tokens) == 2:
message = tokens[0].strip() + '##' + tokens[1].strip()
if message and not message in sentences:
sentences[message] = message.replace("@markmin\x01", "")
add_message(message)
gluon_msg = [Auth.default_messages, Crud.default_messages]
for item in [x for m in gluon_msg for x in m.values() if x is not None]:
add_message(item)
if not '!langcode!' in sentences:
sentences['!langcode!'] = (
DEFAULT_LANGUAGE if language in ('default', DEFAULT_LANGUAGE) else language)
+9 -6
View File
@@ -19,13 +19,14 @@ import sched
import re
import datetime
import platform
import gluon.fileutils
from functools import reduce
try:
import cPickle as pickle
except:
import pickle
from gluon.settings import global_settings
from gluon import fileutils
from gluon._compat import to_bytes
from pydal.contrib import portalocker
logger = logging.getLogger("web2py.cron")
@@ -116,7 +117,7 @@ class Token(object):
def __init__(self, path):
self.path = os.path.join(path, 'cron.master')
if not os.path.exists(self.path):
fileutils.write_file(self.path, '', 'wb')
fileutils.write_file(self.path, to_bytes(''), 'wb')
self.master = None
self.now = time.time()
@@ -139,7 +140,7 @@ class Token(object):
if portalocker.LOCK_EX is None:
logger.warning('WEB2PY CRON: Disabled because no file locking')
return None
self.master = open(self.path, 'rb+')
self.master = fileutils.open_file(self.path, 'rb+')
try:
ret = None
portalocker.lock(self.master, portalocker.LOCK_EX)
@@ -167,6 +168,7 @@ class Token(object):
"""
Writes into cron.master the time when cron job was completed
"""
ret = self.master.closed
if not self.master.closed:
portalocker.lock(self.master, portalocker.LOCK_EX)
logger.debug('WEB2PY CRON: Releasing cron lock')
@@ -177,6 +179,7 @@ class Token(object):
pickle.dump((self.now, time.time()), self.master)
portalocker.unlock(self.master)
self.master.close()
return ret
def rangetolist(s, period='min'):
@@ -222,8 +225,8 @@ def parsecronline(line):
params = line.strip().split(None, 6)
if len(params) < 7:
return None
daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4,
'fri': 5, 'sat': 6}
daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3,
'thu': 4, 'fri': 5, 'sat': 6}
for (s, id) in zip(params[:5], ['min', 'hr', 'dom', 'mon', 'dow']):
if not s in [None, '*']:
task[id] = []
@@ -236,7 +239,7 @@ def parsecronline(line):
elif val.isdigit() or val == '-1':
task[id].append(int(val))
elif id == 'dow' and val[:3].lower() in daysofweek:
task[id].append(daysofweek(val[:3].lower()))
task[id].append(daysofweek[val[:3].lower()])
task['user'] = params[5]
task['cmd'] = params[6]
return task
+4 -1
View File
@@ -137,7 +137,10 @@ class RestrictedError(Exception):
self.environment = environment
if layer:
try:
self.traceback = traceback.format_exc()
try:
self.traceback = traceback.format_exc()
except:
self.traceback = traceback.format_exc(limit=1)
except:
self.traceback = 'no traceback because template parsing error'
try:
+2 -2
View File
@@ -317,7 +317,7 @@ def load(routes='routes.py', app=None, data=None, rdict=None):
symbols = dict(app=app)
try:
exec(data + '\n', symbols)
exec(data, symbols)
except SyntaxError as e:
logger.error(
'%s has a syntax error and will not be loaded\n' % path
@@ -1040,7 +1040,7 @@ class MapUrlIn(object):
else:
default_function = self.router.default_function # str or None
default_function = self.domain_function or default_function
if not arg0 or functions and arg0 not in functions:
if not arg0 or functions and arg0.split('.')[0] not in functions:
self.function = default_function or ""
self.pop_arg_if(arg0 and self.function == arg0)
else:
+4 -5
View File
@@ -307,13 +307,13 @@ try:
except ImportError:
has_futures = False
class Future:
class Future(object):
pass
class ThreadPoolExecutor:
class ThreadPoolExecutor(object):
pass
class _WorkItem:
class _WorkItem(object):
pass
@@ -784,8 +784,7 @@ class Rocket(object):
the application developer. Please update your \
applications to no longer call rocket.stop(True)"
try:
import warnings
raise warnings.DeprecationWarning(msg)
raise DeprecationWarning(msg)
except ImportError:
raise RuntimeError(msg)
+2 -2
View File
@@ -11,7 +11,7 @@ Cross-site scripting (XSS) defense
"""
from gluon._compat import HTMLParser, urlparse, entitydefs, basestring
from cgi import escape
from gluon.utils import local_html_escape
from formatter import AbstractFormatter
from xml.sax.saxutils import quoteattr
@@ -21,7 +21,7 @@ __all__ = ['sanitize']
def xssescape(text):
"""Gets rid of < and > and & and, for good measure, :"""
return escape(text, quote=True).replace(':', '&#58;')
return local_html_escape(text, quote=True).replace(':', '&#58;')
class XssCleaner(HTMLParser):
+2 -2
View File
@@ -1158,7 +1158,7 @@ class Scheduler(MetaScheduler):
if not self.db_thread:
logger.debug('thread building own DAL object')
self.db_thread = DAL(
self.db._uri, folder=self.db._adapter.folder)
self.db._uri, folder=self.db._adapter.folder, decode_credentials=True)
self.define_tables(self.db_thread, migrate=False)
try:
db = self.db_thread
@@ -1698,7 +1698,7 @@ def main():
print('groups for this worker: ' + ', '.join(group_names))
print('connecting to database in folder: ' + options.db_folder or './')
print('using URI: ' + options.db_uri)
db = DAL(options.db_uri, folder=options.db_folder)
db = DAL(options.db_uri, folder=options.db_folder, decode_credentials=True)
print('instantiating scheduler...')
scheduler = Scheduler(db=db,
worker_name=options.worker_name,
+32 -27
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, Set, Rows
from pydal.adapters.base import CALLABLETYPES
from pydal.helpers.methods import smart_query, bar_encode, _repr_ref
from pydal.helpers.methods import smart_query, bar_encode, _repr_ref, merge_tablemaps
from pydal.helpers.classes import Reference, SQLCustomType
from gluon.storage import Storage
from gluon.utils import md5_hash
@@ -2153,7 +2153,8 @@ class SQLFORM(FORM):
ignore_common_filters=None,
auto_pagination=True,
use_cursor=False,
represent_none=None):
represent_none=None,
showblobs=False):
formstyle = formstyle or current.response.formstyle
if isinstance(query, Set):
@@ -2194,7 +2195,7 @@ class SQLFORM(FORM):
buttondelete='icon trash icon-trash glyphicon glyphicon-trash',
buttonedit='icon pen icon-pencil glyphicon glyphicon-pencil',
buttontable='icon rightarrow icon-arrow-right glyphicon glyphicon-arrow-right',
buttonview='icon magnifier icon-zoom-in glyphicon glyphicon-zoom-in',
buttonview='icon magnifier icon-zoom-in glyphicon glyphicon-zoom-in'
)
elif not isinstance(ui, dict):
raise RuntimeError('SQLFORM.grid ui argument must be a dictionary')
@@ -2328,7 +2329,7 @@ class SQLFORM(FORM):
if not isinstance(left, (list, tuple)):
left = [left]
for join in left:
tablenames += db._adapter.tables(join)
tablenames = merge_tablemaps(tablenames, db._adapter.tables(join))
tables = [db[tablename] for tablename in tablenames]
if fields:
# add missing tablename to virtual fields
@@ -2340,7 +2341,7 @@ class SQLFORM(FORM):
else:
fields = []
columns = []
filter1 = lambda f: isinstance(f, Field) and f.type != 'blob'
filter1 = lambda f: isinstance(f, Field) and (f.type!='blob' or showblobs)
filter2 = lambda f: isinstance(f, Field) and f.readable
for table in tables:
fields += filter(filter1, table)
@@ -2553,6 +2554,7 @@ class SQLFORM(FORM):
if isinstance(field, Field.Virtual) and not str(field) in expcolumns:
expcolumns.append(str(field))
expcolumns = ['"%s"' % '"."'.join(f.split('.')) for f in expcolumns]
if export_type in exportManager and exportManager[export_type]:
if keywords:
try:
@@ -2862,7 +2864,7 @@ class SQLFORM(FORM):
for field in columns:
if not field.readable:
continue
if field.type == 'blob':
elif field.type == 'blob' and not showblobs:
continue
if isinstance(field, Field.Virtual) and field.tablename in row:
try:
@@ -3150,7 +3152,9 @@ class SQLFORM(FORM):
# if isinstance(linked_tables, dict):
# linked_tables = linked_tables.get(table._tablename, [])
if linked_tables is None or referee in linked_tables:
field.represent = lambda id, r=None, referee=referee, rep=field.represent: A(callable(rep) and rep(id) or id, cid=request.cid, _href=url(args=['view', referee, id]))
field.represent = (lambda id, r=None, referee=referee, rep=field.represent:
A(callable(rep) and rep(id) or id,
cid=request.cid, _href=url(args=['view', referee, id])))
except (KeyError, ValueError, TypeError):
redirect(URL(args=table._tablename))
if nargs == len(args) + 1:
@@ -3311,23 +3315,28 @@ class SQLTABLE(TABLE):
if not sqlrows:
return
REGEX_TABLE_DOT_FIELD = sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD
fieldmap = dict(zip(sqlrows.colnames, sqlrows.fields))
tablemap = dict(((f.tablename, f.table) for f in fieldmap.values()))
for table in tablemap.values():
pref = table._tablename + '.'
fieldmap.update(((pref+f.name, f) for f in table._virtual_fields))
fieldmap.update(((pref+f.name, f) for f in table._virtual_methods))
field_types = (Field, Field.Virtual, Field.Method)
if not columns:
columns = list(sqlrows.colnames)
if headers == 'fieldname:capitalize':
header_func = {
'fieldname:capitalize': lambda f: f.name.replace('_', ' ').title(),
'labels': lambda f: f.label
}
if isinstance(headers, str) and headers in header_func:
make_name = header_func[headers]
headers = {}
for c in columns:
tfmatch = REGEX_TABLE_DOT_FIELD.match(c)
if tfmatch:
(t, f) = REGEX_TABLE_DOT_FIELD.match(c).groups()
headers[t + '.' + f] = f.replace('_', ' ').title()
f = fieldmap.get(c)
if isinstance(f, field_types):
headers[c] = make_name(f)
else:
headers[c] = REGEX_ALIAS_MATCH.sub(r'\2', c)
elif headers == 'labels':
headers = {}
for c in columns:
(t, f) = c.split('.')
field = sqlrows.db[t][f]
headers[c] = field.label
if colgroup:
cols = [COL(_id=c.replace('.', '-'), data={'column': i + 1})
for i, c in enumerate(columns)]
@@ -3380,9 +3389,8 @@ class SQLTABLE(TABLE):
_class += ' rowselected'
for colname in columns:
matched_column_field = \
sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD.match(colname)
if not matched_column_field:
field = fieldmap.get(colname)
if not isinstance(field, field_types):
if "_extra" in record and colname in record._extra:
r = record._extra[colname]
row.append(TD(r))
@@ -3390,12 +3398,9 @@ class SQLTABLE(TABLE):
else:
raise KeyError(
"Column %s not found (SQLTABLE)" % colname)
(tablename, fieldname) = matched_column_field.groups()
colname = tablename + '.' + fieldname
try:
field = sqlrows.db[tablename][fieldname]
except (KeyError, AttributeError):
field = None
# Virtual fields don't have parent table name...
tablename = colname.split('.', 1)[0]
fieldname = field.name
if tablename in record \
and isinstance(record, Row) \
and isinstance(record[tablename], Row):
+3 -1
View File
@@ -13,6 +13,7 @@ from .test_contribs import *
from .test_routes import *
from .test_router import *
from .test_validators import *
from .test_authapi import *
from .test_tools import *
from .test_utils import *
from .test_serializers import *
@@ -22,7 +23,8 @@ from .test_appadmin import *
from .test_web import *
from .test_sqlhtml import *
from .test_scheduler import *
from .test_cron import *
from .test_is_url import *
if sys.version[:3] == '2.7':
from .test_is_url import *
from .test_old_doctests import *
+8 -4
View File
@@ -91,7 +91,8 @@ class TestAppAdmin(unittest.TestCase):
self.run_view()
self.run_view_file_stream()
except Exception as e:
print(e.message)
import traceback
print(traceback.format_exc())
self.fail('Could not make the view')
def test_index(self):
@@ -115,7 +116,8 @@ class TestAppAdmin(unittest.TestCase):
try:
self.run_view()
except Exception as e:
print(e.message)
import traceback
print(traceback.format_exc())
self.fail('Could not make the view')
def test_insert(self):
@@ -129,7 +131,8 @@ class TestAppAdmin(unittest.TestCase):
try:
self.run_view()
except Exception as e:
print(e.message)
import traceback
print(traceback.format_exc())
self.fail('Could not make the view')
def test_insert_submit(self):
@@ -151,7 +154,8 @@ class TestAppAdmin(unittest.TestCase):
try:
self.run_view()
except Exception as e:
print(e.message)
import traceback
print(traceback.format_exc())
self.fail('Could not make the view')
db = self.env['db']
lisa_record = db(db.auth_user.username == 'lisasimpson').select().first()
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Unit tests for authapi """
import os
import unittest
from gluon.globals import Request, Response, Session
from gluon.languages import translator
from gluon.dal import DAL, Field
from gluon.authapi import AuthAPI
from gluon.storage import Storage
from gluon._compat import to_bytes, to_native, add_charset
DEFAULT_URI = os.getenv('DB', 'sqlite:memory')
class TestAuthAPI(unittest.TestCase):
def setUp(self):
self.request = Request(env={})
self.request.application = 'a'
self.request.controller = 'c'
self.request.function = 'f'
self.request.folder = 'applications/admin'
self.response = Response()
self.session = Session()
T = translator('', 'en')
self.session.connect(self.request, self.response)
from gluon.globals import current
self.current = current
self.current.request = self.request
self.current.response = self.response
self.current.session = self.session
self.current.T = T
self.db = DAL(DEFAULT_URI, check_reserved=['all'])
self.auth = AuthAPI(self.db)
self.auth.define_tables(username=True, signature=False)
# Create a user
self.auth.table_user().validate_and_insert(first_name='Bart',
last_name='Simpson',
username='bart',
email='bart@simpson.com',
password='bart_password',
registration_key='',
registration_id=''
)
self.db.commit()
def test_login(self):
result = self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
self.assertTrue(result['user']['email'] == 'bart@simpson.com')
self.auth.logout()
self.assertFalse(self.auth.is_logged_in())
self.auth.settings.username_case_sensitive = False
result = self.auth.login(**{'username': 'BarT', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
def test_logout(self):
self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
result = self.auth.logout()
self.assertTrue(not self.auth.is_logged_in())
self.assertTrue(result['user'] is None)
def test_register(self):
self.auth.settings.login_after_registration = True
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
self.assertTrue(result['user']['email'] == 'lisa@simpson.com')
self.assertTrue(self.auth.is_logged_in())
with self.assertRaises(AssertionError): # Can't register if you're logged in
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
self.auth.logout()
self.auth.settings.login_after_registration = False
result = self.auth.register(**{
'username': 'barney',
'first_name': 'Barney',
'last_name': 'Gumble',
'email': 'barney@simpson.com',
'password': 'barney_password'
})
self.assertTrue(result['user']['email'] == 'barney@simpson.com')
self.assertFalse(self.auth.is_logged_in())
self.auth.settings.login_userfield = 'email'
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
self.assertTrue(result['errors']['email'] == self.auth.messages.email_taken)
self.assertTrue(result['user'] is None)
self.auth.settings.registration_requires_verification = True
result = self.auth.register(**{
'username': 'homer',
'first_name': 'Homer',
'last_name': 'Simpson',
'email': 'homer@simpson.com',
'password': 'homer_password'
})
self.assertTrue('key' in result['user'])
def test_profile(self):
with self.assertRaises(AssertionError):
# We are not logged in
self.auth.profile()
self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
result = self.auth.profile(email='bartolo@simpson.com')
self.assertTrue(result['user']['email'] == 'bartolo@simpson.com')
self.assertTrue(self.auth.table_user()[result['user']['id']].email == 'bartolo@simpson.com')
def test_change_password(self):
with self.assertRaises(AssertionError):
# We are not logged in
self.auth.change_password()
self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234')
self.auth.logout()
self.assertTrue(not self.auth.is_logged_in())
self.auth.login(username='bart', password='1234')
self.assertTrue(self.auth.is_logged_in())
result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='5678')
self.assertTrue('new_password2' in result['errors'])
result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234')
self.assertTrue('old_password' in result['errors'])
def test_verify_key(self):
self.auth.settings.registration_requires_verification = True
result = self.auth.register(**{
'username': 'homer',
'first_name': 'Homer',
'last_name': 'Simpson',
'email': 'homer@simpson.com',
'password': 'homer_password'
})
self.assertTrue('key' in result['user'])
homer_id = result['user']['id']
homers_key = result['user']['key']
result = self.auth.verify_key(key=None)
self.assertTrue(result['errors'] is not None)
result = self.auth.verify_key(key='12345')
self.assertTrue(result['errors'] is not None)
result = self.auth.verify_key(key=homers_key)
self.assertTrue(result['errors'] is None)
self.assertEqual(self.auth.table_user()[homer_id].registration_key, '')
self.auth.settings.registration_requires_approval = True
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
lisa_id = result['user']['id']
result = self.auth.verify_key(key=result['user']['key'])
self.assertEqual(self.auth.table_user()[lisa_id].registration_key, 'pending')
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Unit tests for cron """
import unittest, os
from gluon.newcron import Token, crondance
class TestCron(unittest.TestCase):
def test_Token(self):
appname_path = os.path.join(os.getcwd(), 'applications', 'welcome')
t = Token(path=appname_path)
self.assertNotEqual(t.acquire(), None)
self.assertFalse(t.release())
self.assertEqual(t.acquire(), None)
self.assertTrue(t.release())
return
def test_crondance(self):
#TODO update crondance to return something
crondance(os.getcwd())
+72 -24
View File
@@ -10,27 +10,76 @@ import re
import unittest
from gluon.globals import Request, Response, Session
from gluon.rewrite import regex_url_in
from gluon import URL
from gluon._compat import basestring
def setup_clean_session():
request = Request(env={})
request.application = 'a'
request.controller = 'c'
request.function = 'f'
request.folder = 'applications/admin'
response = Response()
session = Session()
session.connect(request, response)
request = Request(env={})
request.application = 'a'
request.controller = 'c'
request.function = 'f'
request.folder = 'applications/admin'
response = Response()
session = Session()
session.connect(request, response)
from gluon.globals import current
current.request = request
current.response = response
current.session = session
return current
class testRequest(unittest.TestCase):
def setUp(self):
from gluon.globals import current
current.request = request
current.response = response
current.session = session
return current
current.response = Response()
def test_restful_simple(self):
env = {'request_method': 'GET', 'PATH_INFO': '/welcome/default/index/1.pdf'}
r = Request(env)
regex_url_in(r, env)
@r.restful()
def simple_rest():
def GET(*args, **vars):
return args[0]
return locals()
self.assertEqual(simple_rest(), '1')
def test_restful_calls_post(self):
env = {'request_method': 'POST', 'PATH_INFO': '/welcome/default/index'}
r = Request(env)
regex_url_in(r, env)
@r.restful()
def post_rest():
def POST(*args, **vars):
return 'I posted'
return locals()
self.assertEqual(post_rest(), 'I posted')
def test_restful_ignore_extension(self):
env = {'request_method': 'GET', 'PATH_INFO': '/welcome/default/index/127.0.0.1'}
r = Request(env)
regex_url_in(r, env)
@r.restful(ignore_extension=True)
def ignore_rest():
def GET(*args, **vars):
return args[0]
return locals()
self.assertEqual(ignore_rest(), '127.0.0.1')
class testResponse(unittest.TestCase):
#port from python 2.7, needed for 2.5 and 2.6 tests
# port from python 2.7, needed for 2.5 and 2.6 tests
def assertRegexpMatches(self, text, expected_regexp, msg=None):
"""Fail the test unless the text matches the regular expression."""
if isinstance(expected_regexp, basestring):
@@ -99,8 +148,8 @@ class testResponse(unittest.TestCase):
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" />')
'<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'))
@@ -109,12 +158,11 @@ class testResponse(unittest.TestCase):
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>'
)
'<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'))
@@ -122,13 +170,13 @@ class testResponse(unittest.TestCase):
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
# 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
# regr test for #628
response = Response()
response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
content = return_includes(response)
@@ -147,7 +195,7 @@ class testResponse(unittest.TestCase):
def test_cookies(self):
current = setup_clean_session()
cookie = str(current.response.cookies)
session_key='%s=%s'%(current.response.session_id_name,current.response.session_id)
session_key = '%s=%s' % (current.response.session_id_name, current.response.session_id)
self.assertRegexpMatches(cookie, r'^Set-Cookie: ')
self.assertTrue(session_key in cookie)
self.assertTrue('Path=/' in cookie)
+50 -13
View File
@@ -11,11 +11,13 @@ import unittest
from gluon.html import A, ASSIGNJS, B, BEAUTIFY, P, BODY, BR, BUTTON, CAT, CENTER, CODE, COL, COLGROUP, DIV, SPAN, URL, verifyURL
from gluon.html import truncate_string, EM, FIELDSET, FORM, H1, H2, H3, H4, H5, H6, HEAD, HR, HTML, I, IFRAME, IMG, INPUT, EMBED
from gluon.html import LABEL, LEGEND, LI, LINK, MARKMIN, MENU, META, OBJECT, OL, OPTGROUP, OPTION, PRE, SCRIPT, SELECT, STRONG
from gluon.html import STYLE, TABLE, TR, TD, TAG, TBODY, THEAD, TEXTAREA, TFOOT, TH, TITLE, TT, UL, XHTML, XML
from gluon.html import STYLE, TABLE, TR, TD, TAG, TBODY, THEAD, TEXTAREA, TFOOT, TH, TITLE, TT, UL, XHTML, XML, web2pyHTMLParser
from gluon.storage import Storage
from gluon.html import XML_pickle, XML_unpickle
from gluon.html import TAG_pickler, TAG_unpickler
from gluon._compat import xrange, PY2, to_native
from gluon.decoder import decoder
import re
class TestBareHelpers(unittest.TestCase):
@@ -155,7 +157,7 @@ class TestBareHelpers(unittest.TestCase):
self.assertEqual(rtn, True)
# TODO: def test_XmlComponent(self):
@unittest.skipIf(not PY2, "Skipping Python 3.x tests for XML.__repr__")
def test_XML(self):
# sanitization process
self.assertEqual(XML('<h1>Hello<a data-hello="world">World</a></h1>').xml(),
@@ -168,6 +170,8 @@ class TestBareHelpers(unittest.TestCase):
# seams that __repr__ is no longer enough
##self.assertEqual(XML('1.3'), '1.3')
self.assertEqual(XML(u'<div>è</div>').xml(), b'<div>\xc3\xa8</div>')
# make sure unicode works with sanitize
self.assertEqual(XML(u'<div>è</div>', sanitize=True).xml(), b'<div>\xc3\xa8</div>')
# you can calc len on the class, that equals the xml() and the str()
##self.assertEqual(len(XML('1.3')), len('1.3'))
self.assertEqual(len(XML('1.3').xml()), len('1.3'))
@@ -179,19 +183,18 @@ class TestBareHelpers(unittest.TestCase):
# you can compare them
##self.assertEqual(XML('a') == XML('a'), True)
# beware that the comparison is made on the XML repr
self.assertEqual(XML('<h1>Hello<a data-hello="world">World</a></h1>', sanitize=True),
XML('<h1>HelloWorld</h1>'))
self.assertEqual(XML('<h1>Hello<a data-hello="world">World</a></h1>', sanitize=True).__repr__(),
XML('<h1>HelloWorld</h1>').__repr__())
# bug check for the sanitizer for closing no-close tags
self.assertEqual(XML('<p>Test</p><br/><p>Test</p><br/>', sanitize=True),
XML('<p>Test</p><br /><p>Test</p><br />'))
self.assertEqual(XML('<p>Test</p><br/><p>Test</p><br/>', sanitize=True).xml(),
XML('<p>Test</p><br /><p>Test</p><br />').xml())
# basic flatten test
self.assertEqual(XML('<p>Test</p>').flatten(), '<p>Test</p>')
self.assertEqual(XML('<p>Test</p>').flatten(render=lambda text, tag, attr: text), '<p>Test</p>')
@unittest.skipIf(not PY2, "Skipping Python 3.x tests for XML_unpickle.__repr__")
def test_XML_pickle_unpickle(self):
# weird test
self.assertEqual(XML_unpickle(XML_pickle('data to be pickle')[1][0]), 'data to be pickle')
self.assertEqual(str(XML_unpickle(XML_pickle('data to be pickle')[1][0])), 'data to be pickle')
def test_DIV(self):
# Empty DIV()
@@ -255,6 +258,11 @@ class TestBareHelpers(unittest.TestCase):
self.assertEqual(DIV('<p>Test</p>', _class="class_test").get('_class'), 'class_test')
self.assertEqual(DIV(b'a').xml(), b'<div>a</div>')
def test_decoder(self):
tag_html = '<div><span><a id="1-1" u:v="$">hello</a></span><p class="this is a test">world</p></div>'
a = decoder(tag_html)
self.assertEqual(a, tag_html)
def test_CAT(self):
# Empty CAT()
self.assertEqual(CAT().xml(), b'')
@@ -636,8 +644,8 @@ class TestBareHelpers(unittest.TestCase):
# These 2 crash AppVeyor and Travis with: "ImportError: No YAML serializer available"
# self.assertEqual(FORM('<>', _a='1', _b='2').as_yaml(),
# "accepted: null\nattributes: {_a: '1', _action: '#', _b: '2', _enctype: multipart/form-data, _method: post}\ncomponents: [<>]\nerrors: {}\nlatest: {}\nparent: null\nvars: {}\n")
# self.assertEqual(FORM('<>', _a='1', _b='2').as_xml(),
# '<?xml version="1.0" encoding="UTF-8"?><document><errors></errors><vars></vars><parent>None</parent><attributes><_enctype>multipart/form-data</_enctype><_action>#</_action><_b>2</_b><_a>1</_a><_method>post</_method></attributes><components><item>&amp;lt;&amp;gt;</item></components><accepted>None</accepted><latest></latest></document>')
# TODO check tags content
self.assertEqual(len(FORM('<>', _a='1', _b='2').as_xml()), 334)
def test_BEAUTIFY(self):
#self.assertEqual(BEAUTIFY(['a', 'b', {'hello': 'world'}]).xml(),
@@ -670,13 +678,42 @@ class TestBareHelpers(unittest.TestCase):
# TODO: def test_embed64(self):
# TODO: def test_web2pyHTMLParser(self):
def test_web2pyHTMLParser(self):
#tag should not be a byte
self.assertEqual(web2pyHTMLParser("<div></div>").tree.components[0].tag, 'div')
a = str(web2pyHTMLParser('<div>a<span>b</div>c').tree)
self.assertEqual(a, "<div>a<span>b</span></div>c")
tree = web2pyHTMLParser('hello<div a="b">world</div>').tree
tree.element(_a='b')['_c']=5
self.assertEqual(str(tree), 'hello<div a="b" c="5">world</div>')
a = str(web2pyHTMLParser('<div><img class="img"/></div>', closed=['img']).tree)
self.assertEqual(a, '<div><img class="img" /></div>')
#greater-than sign ( > ) --> decimal &#62; --> hexadecimal &#x3E;
#Less-than sign ( < ) --> decimal &#60; --> hexadecimal &#x3C;
# test decimal
a = str(web2pyHTMLParser('<div>&#60; &#62;</div>').tree)
self.assertEqual(a, '<div>&lt; &gt;</div>')
# test hexadecimal
a = str(web2pyHTMLParser('<div>&#x3C; &#x3E;</div>').tree)
self.assertEqual(a, '<div>&lt; &gt;</div>')
def test_markdown(self):
def markdown(text, tag=None, attributes={}):
r = {None: re.sub('\s+',' ',text), \
'h1':'#'+text+'\\n\\n', \
'p':text+'\\n'}.get(tag,text)
return r
a=TAG('<h1>Header</h1><p>this is a test</p>')
ret = a.flatten(markdown)
self.assertEqual(ret, '#Header\\n\\nthis is a test\\n')
# TODO: def test_markdown_serializer(self):
# TODO: def test_markmin_serializer(self):
@unittest.skipIf(not PY2, "Skipping Python 3.x tests for MARKMIN")
def test_MARKMIN(self):
# This test pass with python 2.7 but expected to fail under 2.6
# with self.assertRaises(TypeError) as cm:
+39 -39
View File
@@ -586,81 +586,81 @@ class TestUnicode(unittest.TestCase):
# disables prepending the scheme in the return value
def testUnicodeToAsciiUrl(self):
self.assertEquals(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu')
self.assertEquals(
self.assertEqual(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu')
self.assertEqual(
unicode_to_ascii_authority(u'www.benn.ca'), 'www.benn.ca')
self.assertRaises(UnicodeError, unicode_to_ascii_authority,
u'\u4e2d' * 1000) # label is too long
def testValidUrls(self):
self.assertEquals(self.x(u'www.Alliancefrancaise.nu'), (
self.assertEqual(self.x(u'www.Alliancefrancaise.nu'), (
'http://www.Alliancefrancaise.nu', None))
self.assertEquals(self.x(u'www.Alliancefran\xe7aise.nu'), (
self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu'), (
'http://www.xn--alliancefranaise-npb.nu', None))
self.assertEquals(self.x(u'www.Alliancefran\xe7aise.nu:8080'), (
self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu:8080'), (
'http://www.xn--alliancefranaise-npb.nu:8080', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu'),
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu'),
('http://www.xn--alliancefranaise-npb.nu', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue#fragment', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu:8080/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu:8080/parnaise/blue?query=value#fragment', None))
self.assertEquals(self.x(u'www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
self.assertEquals(self.x(
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue', None))
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue#fragment', None))
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu:8080/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu:8080/parnaise/blue?query=value#fragment', None))
self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
self.assertEqual(self.x(
u'http://\u4e2d\u4fd4.com'), ('http://xn--fiq13b.com', None))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com/\u4e86'),
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86'),
('http://xn--fiq13b.com/%4e%86', None))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86', None))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86#fragment'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86#fragment', None))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com?query=\u4e86#fragment'), ('http://xn--fiq13b.com?query=%4e%86#fragment', None))
self.assertEquals(
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86', None))
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86#fragment'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86#fragment', None))
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com?query=\u4e86#fragment'), ('http://xn--fiq13b.com?query=%4e%86#fragment', None))
self.assertEqual(
self.x(u'http://B\xfccher.ch'), ('http://xn--bcher-kva.ch', None))
self.assertEquals(self.x(u'http://\xe4\xf6\xfc\xdf.com'), (
self.assertEqual(self.x(u'http://\xe4\xf6\xfc\xdf.com'), (
'http://xn--ss-uia6e4a.com', None))
self.assertEquals(self.x(
self.assertEqual(self.x(
u'http://visegr\xe1d.com'), ('http://xn--visegrd-mwa.com', None))
self.assertEquals(self.x(u'http://h\xe1zipatika.com'), (
self.assertEqual(self.x(u'http://h\xe1zipatika.com'), (
'http://xn--hzipatika-01a.com', None))
self.assertEquals(self.x(u'http://www.\xe7ukurova.com'), (
self.assertEqual(self.x(u'http://www.\xe7ukurova.com'), (
'http://www.xn--ukurova-txa.com', None))
self.assertEquals(self.x(u'http://nixier\xf6hre.nixieclock-tube.com'), ('http://xn--nixierhre-57a.nixieclock-tube.com', None))
self.assertEquals(self.x(u'google.ca.'), ('http://google.ca.', None))
self.assertEqual(self.x(u'http://nixier\xf6hre.nixieclock-tube.com'), ('http://xn--nixierhre-57a.nixieclock-tube.com', None))
self.assertEqual(self.x(u'google.ca.'), ('http://google.ca.', None))
self.assertEquals(
self.assertEqual(
self.y(u'https://google.ca'), ('https://google.ca', None))
self.assertEquals(self.y(
self.assertEqual(self.y(
u'https://\u4e2d\u4fd4.com'), ('https://xn--fiq13b.com', None))
self.assertEquals(self.z(u'google.ca'), ('google.ca', None))
self.assertEqual(self.z(u'google.ca'), ('google.ca', None))
def testInvalidUrls(self):
self.assertEquals(
self.assertEqual(
self.x(u'://ABC.com'), (u'://ABC.com', 'Enter a valid URL'))
self.assertEquals(self.x(u'http://\u4e2d\u4fd4.dne'), (
self.assertEqual(self.x(u'http://\u4e2d\u4fd4.dne'), (
u'http://\u4e2d\u4fd4.dne', 'Enter a valid URL'))
self.assertEquals(self.x(u'https://google.dne'), (
self.assertEqual(self.x(u'https://google.dne'), (
u'https://google.dne', 'Enter a valid URL'))
self.assertEquals(self.x(u'https://google..ca'), (
self.assertEqual(self.x(u'https://google..ca'), (
u'https://google..ca', 'Enter a valid URL'))
self.assertEquals(
self.assertEqual(
self.x(u'google..ca'), (u'google..ca', 'Enter a valid URL'))
self.assertEquals(self.x(u'http://' + u'\u4e2d' * 1000 + u'.com'), (
self.assertEqual(self.x(u'http://' + u'\u4e2d' * 1000 + u'.com'), (
u'http://' + u'\u4e2d' * 1000 + u'.com', 'Enter a valid URL'))
self.assertEquals(self.x(u'http://google.com#fragment_\u4e86'), (
self.assertEqual(self.x(u'http://google.com#fragment_\u4e86'), (
u'http://google.com#fragment_\u4e86', 'Enter a valid URL'))
self.assertEquals(self.x(u'http\u4e86://google.com'), (
self.assertEqual(self.x(u'http\u4e86://google.com'), (
u'http\u4e86://google.com', 'Enter a valid URL'))
self.assertEquals(self.x(u'http\u4e86://google.com#fragment_\u4e86'), (
self.assertEqual(self.x(u'http\u4e86://google.com#fragment_\u4e86'), (
u'http\u4e86://google.com#fragment_\u4e86', 'Enter a valid URL'))
self.assertEquals(self.y(u'http://\u4e2d\u4fd4.com/\u4e86'), (
self.assertEqual(self.y(u'http://\u4e2d\u4fd4.com/\u4e86'), (
u'http://\u4e2d\u4fd4.com/\u4e86', 'Enter a valid URL'))
#self.assertEquals(self.y(u'google.ca'), (u'google.ca', 'Enter a valid URL'))
#self.assertEqual(self.y(u'google.ca'), (u'google.ca', 'Enter a valid URL'))
self.assertEquals(self.z(u'invalid.domain..com'), (
self.assertEqual(self.z(u'invalid.domain..com'), (
u'invalid.domain..com', 'Enter a valid URL'))
self.assertEquals(self.z(u'invalid.\u4e2d\u4fd4.blargg'), (
self.assertEqual(self.z(u'invalid.\u4e2d\u4fd4.blargg'), (
u'invalid.\u4e2d\u4fd4.blargg', 'Enter a valid URL'))
# ##############################################################################
+47 -2
View File
@@ -95,14 +95,14 @@ class TestRouter(unittest.TestCase):
""" Test router syntax error """
level = logger.getEffectiveLevel()
logger.setLevel(logging.CRITICAL) # disable logging temporarily
self.assertRaises(SyntaxError, load, data='x:y')
self.assertRaises(SyntaxError, load, data='x::y')
self.assertRaises(
SyntaxError, load, rdict=dict(BASE=dict(badkey="value")))
self.assertRaises(SyntaxError, load, rdict=dict(
BASE=dict(), app=dict(default_application="name")))
self.myassertRaisesRegex(SyntaxError, "invalid syntax",
load, data='x:y')
load, data='x::y')
self.myassertRaisesRegex(SyntaxError, "unknown key",
load, rdict=dict(BASE=dict(badkey="value")))
self.myassertRaisesRegex(SyntaxError, "BASE-only key",
@@ -873,6 +873,27 @@ class TestRouter(unittest.TestCase):
self.assertEqual(str(URL(a='app2', c='default', f='index',
args=['ctr'])), "/app2/index/ctr")
# outbound - with extensions
self.assertEqual(
str(URL(a='app', c='default', f='index.json')), "/index.json")
self.assertEqual(
str(URL(a='app', c='default', f='index.json', args=['arg1'])), "/index.json/arg1")
self.assertEqual(str(
URL(a='app', c='default', f='user.json')), "/user.json")
self.assertEqual(str(
URL(a='app', c='default', f='user.json', args=['arg1'])), "/user.json/arg1")
self.assertEqual(str(URL(
a='app', c='default', f='user.json', args=['index'])), "/user.json/index")
self.assertEqual(str(
URL(a='app', c='default', f='index.json', args=['ctr'])), "/index.json/ctr")
self.assertEqual(
str(URL(a='app', c='ctr', f='ctrf1.json', args=['arg'])), "/ctr/ctrf1.json/arg")
self.assertEqual(str(URL(
a='app2', c='default', f='index.json', args=['arg1'])), "/app2/index.json/arg1")
self.assertEqual(str(URL(
a='app2', c='ctr', f='index.json', args=['arg1'])), "/app2/ctr/index.json/arg1")
# inbound
self.assertEqual(
filter_url('http://d.com/arg'), "/app/default/index ['arg']")
@@ -896,6 +917,30 @@ class TestRouter(unittest.TestCase):
self.assertEqual(
filter_url('http://d.com/app2/ctr/arg'), "/app2/ctr/arg")
# inbound - with extensions
self.assertEqual(
filter_url('http://d.com/index.json'), "/app/default/index.json")
self.assertEqual(filter_url('http://d.com/user.json'), "/app/default/user.json")
self.assertEqual(
filter_url('http://d.com/user.json/arg'), "/app/default/user.json ['arg']")
self.assertEqual(
filter_url('http://d.com/user.json/index'), "/app/default/user.json ['index']")
self.assertEqual(
filter_url('http://d.com/index.json/ctr'), "/app/default/index.json ['ctr']")
self.assertEqual(
filter_url('http://d.com/ctr/ctrf1.json/arg'), "/app/ctr/ctrf1.json ['arg']")
self.assertEqual(filter_url(
'http://d.com/app2/index.json/arg'), "/app2/default/index.json ['arg']")
self.assertEqual(
filter_url('http://d.com/app2/user.json'), "/app2/default/user.json")
self.assertEqual(filter_url(
'http://d.com/app2/user.json/arg'), "/app2/default/user.json ['arg']")
self.assertEqual(filter_url(
'http://d.com/app2/ctr/index.json/arg'), "/app2/ctr/index.json ['arg']")
self.assertEqual(
filter_url('http://d.com/app2/ctr/arg'), "/app2/ctr/arg")
def test_router_functions2(self):
'''
Test more functions=[something]
+28 -15
View File
@@ -235,6 +235,11 @@ class TestValidators(unittest.TestCase):
self.assertEqual(sorted(rtn), [('%d' % george_id, 'george'), ('%d' % costanza_id, 'costanza')])
rtn = IS_IN_DB(db, db.person.id, db.person.name, error_message='oops', sort=True).options(zero=True)
self.assertEqual(rtn, [('', ''), ('%d' % costanza_id, 'costanza'), ('%d' % george_id, 'george')])
# Test None
rtn = IS_IN_DB(db, 'person.id', '%(name)s', error_message='oops')(None)
self.assertEqual(rtn, (None, 'oops'))
rtn = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops')(None)
self.assertEqual(rtn, (None, 'oops'))
# Test using the set it made for options
vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops')
vldtr.options()
@@ -434,21 +439,21 @@ class TestValidators(unittest.TestCase):
rtn = IS_NOT_EMPTY()('x')
self.assertEqual(rtn, ('x', None))
rtn = IS_NOT_EMPTY()(' x ')
self.assertEqual(rtn, ('x', None))
self.assertEqual(rtn, (' x ', None))
rtn = IS_NOT_EMPTY()(None)
self.assertEqual(rtn, (None, 'Enter a value'))
rtn = IS_NOT_EMPTY()('')
self.assertEqual(rtn, ('', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' ')
self.assertEqual(rtn, ('', 'Enter a value'))
self.assertEqual(rtn, (' ', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' \n\t')
self.assertEqual(rtn, ('', 'Enter a value'))
self.assertEqual(rtn, (' \n\t', 'Enter a value'))
rtn = IS_NOT_EMPTY()([])
self.assertEqual(rtn, ([], 'Enter a value'))
rtn = IS_NOT_EMPTY(empty_regex='def')('def')
self.assertEqual(rtn, ('', 'Enter a value'))
self.assertEqual(rtn, ('def', 'Enter a value'))
rtn = IS_NOT_EMPTY(empty_regex='de[fg]')('deg')
self.assertEqual(rtn, ('', 'Enter a value'))
self.assertEqual(rtn, ('deg', 'Enter a value'))
rtn = IS_NOT_EMPTY(empty_regex='def')('abc')
self.assertEqual(rtn, ('abc', None))
@@ -531,6 +536,11 @@ class TestValidators(unittest.TestCase):
rtn = IS_EMAIL(error_message='oops')(42)
self.assertEqual(rtn, (42, 'oops'))
# test for Internationalized Domain Names, see https://docs.python.org/2/library/codecs.html#module-encodings.idna
rtn = IS_EMAIL()('web2py@Alliancefrançaise.nu')
self.assertEqual(rtn, ('web2py@Alliancefrançaise.nu', None))
def test_IS_LIST_OF_EMAILS(self):
emails = ['localguy@localhost', '_Yosemite.Sam@example.com']
rtn = IS_LIST_OF_EMAILS()(','.join(emails))
@@ -702,15 +712,19 @@ class TestValidators(unittest.TestCase):
def test_IS_LOWER(self):
rtn = IS_LOWER()('ABC')
self.assertEqual(rtn, ('abc', None))
rtn = IS_LOWER()(b'ABC')
self.assertEqual(rtn, (b'abc', None))
rtn = IS_LOWER()('Ñ')
self.assertEqual(rtn, (b'\xc3\xb1', None))
self.assertEqual(rtn, ('ñ', None))
def test_IS_UPPER(self):
rtn = IS_UPPER()('abc')
self.assertEqual(rtn, ('ABC', None))
rtn = IS_UPPER()(b'abc')
self.assertEqual(rtn, (b'ABC', None))
rtn = IS_UPPER()('ñ')
self.assertEqual(rtn, (b'\xc3\x91', None))
self.assertEqual(rtn, ('Ñ', None))
def test_IS_SLUG(self):
rtn = IS_SLUG()('abc123')
@@ -780,7 +794,7 @@ class TestValidators(unittest.TestCase):
rtn = IS_EMPTY_OR(IS_EMAIL())('abc')
self.assertEqual(rtn, ('abc', 'Enter a valid email address'))
rtn = IS_EMPTY_OR(IS_EMAIL())(' abc ')
self.assertEqual(rtn, ('abc', 'Enter a valid email address'))
self.assertEqual(rtn, (' abc ', 'Enter a valid email address'))
rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options(zero=False)
self.assertEqual(rtn, [('', ''), ('id1', 'first label'), ('id2', 'second label')])
rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options()
@@ -843,7 +857,7 @@ class TestValidators(unittest.TestCase):
'|'.join(['Minimum length is 8',
'Maximum length is 4',
'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|',
'Must include at least 1 upper case',
'Must include at least 1 uppercase',
'Must include at least 1 number']))
)
rtn = IS_STRONG(es=True)('abcde')
@@ -851,7 +865,7 @@ class TestValidators(unittest.TestCase):
('abcde',
'|'.join(['Minimum length is 8',
'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|',
'Must include at least 1 upper case',
'Must include at least 1 uppercase',
'Must include at least 1 number']))
)
rtn = IS_STRONG(upper=0, lower=0, number=0, es=True)('Abcde1')
@@ -859,8 +873,8 @@ class TestValidators(unittest.TestCase):
('Abcde1',
'|'.join(['Minimum length is 8',
'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|',
'May not include any upper case letters',
'May not include any lower case letters',
'May not include any uppercase letters',
'May not include any lowercase letters',
'May not include any numbers']))
)
@@ -1029,12 +1043,11 @@ this is the content of the fake file
self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', 'Enter valid IPv6 address'))
rtn = IS_IPV6(is_multicast=True)('ff00::126c:8ffa:fe22:b3af')
self.assertEqual(rtn, ('ff00::126c:8ffa:fe22:b3af', None))
# TODO:
# with py3.ipaddress '2001::126c:8ffa:fe22:b3af' is considered private
# with py2.ipaddress '2001::126c:8ffa:fe22:b3af' is considered private
# with gluon.contrib.ipaddr(both current and trunk) is not considered private
# rtn = IS_IPV6(is_routeable=True)('2001::126c:8ffa:fe22:b3af')
# self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', None))
rtn = IS_IPV6(is_routeable=False)('2001::126c:8ffa:fe22:b3af')
self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', None))
rtn = IS_IPV6(is_routeable=True)('ff00::126c:8ffa:fe22:b3af')
self.assertEqual(rtn, ('ff00::126c:8ffa:fe22:b3af', 'Enter valid IPv6 address'))
rtn = IS_IPV6(subnets='2001::/32')('2001::8ffa:fe22:b3af')
+1 -1
View File
@@ -110,7 +110,7 @@ class TestWeb(LiveTest):
self.assertTrue('Welcome Homer' in client.text)
client = WebClient('http://127.0.0.1:8000/admin/default/')
client.post('index', data=dict(password='hello'))
client.post('index', data=dict(password='testpass'))
client.get('site')
client.get('design/welcome')
+315 -855
View File
File diff suppressed because it is too large Load Diff
+14 -12
View File
@@ -17,6 +17,7 @@ import random
import inspect
import time
import os
import sys
import re
import logging
import socket
@@ -299,19 +300,20 @@ def initialize_urandom():
try:
os.urandom(1)
have_urandom = True
try:
# try to add process-specific entropy
frandom = open('/dev/urandom', 'wb')
if sys.platform != 'win32':
try:
if PY2:
frandom.write(''.join(chr(t) for t in ctokens))
else:
frandom.write(bytes([]).join(bytes([t]) for t in ctokens))
finally:
frandom.close()
except IOError:
# works anyway
pass
# try to add process-specific entropy
frandom = open('/dev/urandom', 'wb')
try:
if PY2:
frandom.write(''.join(chr(t) for t in ctokens))
else:
frandom.write(bytes([]).join(bytes([t]) for t in ctokens))
finally:
frandom.close()
except IOError:
# works anyway
pass
except NotImplementedError:
have_urandom = False
logger.warning(
+129 -114
View File
@@ -10,7 +10,6 @@
Validators
-----------
"""
import os
import re
import datetime
@@ -21,7 +20,8 @@ import urllib
import struct
import decimal
import unicodedata
from gluon._compat import StringIO, long, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native
from gluon._compat import StringIO, long, basestring, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native, string_types, urlparse
from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE
from pydal.objects import Field, FieldVirtual, FieldMethod
from functools import reduce
@@ -195,7 +195,7 @@ class IS_MATCH(Validator):
self.is_unicode = is_unicode or (not(PY2))
def __call__(self, value):
if not(PY2): # PY3 convert bytes to unicode
if not(PY2): # PY3 convert bytes to unicode
value = to_unicode(value)
if self.is_unicode or not(PY2):
@@ -270,7 +270,7 @@ class IS_EXPR(Validator):
return (value, self.expression(value))
# for backward compatibility
self.environment.update(value=value)
exec ('__ret__=' + self.expression, self.environment)
exec('__ret__=' + self.expression, self.environment)
if self.environment['__ret__']:
return (value, None)
return (value, translate(self.error_message))
@@ -659,7 +659,7 @@ class IS_IN_DB(Validator):
return (values, None)
else:
if field.type in ('id', 'integer'):
if isinstance(value, (int, long)) or value.isdigit():
if isinstance(value, (int, long)) or (isinstance(value, string_types) and value.isdigit()):
value = int(value)
elif self.auto_add:
value = self.maybe_add(table, self.fieldnames[0], value)
@@ -989,14 +989,15 @@ class IS_DECIMAL_IN_RANGE(Validator):
def is_empty(value, empty_regex=None):
_value = value
"""test empty field"""
if isinstance(value, (str, unicodeT)):
value = value.strip()
if empty_regex is not None and empty_regex.match(value):
value = ''
if value is None or value == '' or value == []:
return (value, True)
return (value, False)
return (_value, True)
return (_value, False)
class IS_NOT_EMPTY(Validator):
@@ -1156,15 +1157,16 @@ class IS_EMAIL(Validator):
"""
regex = re.compile('''
body_regex = re.compile('''
^(?!\.) # name may not begin with a dot
(
[-a-z0-9!\#$%&'*+/=?^_`{|}~] # all legal characters except dot
|
(?<!\.)\. # single dots only
)+
(?<!\.) # name may not end with a dot
@
(?<!\.)$ # name may not end with a dot
''', re.VERBOSE | re.IGNORECASE)
domain_regex = re.compile('''
(
localhost
|
@@ -1196,14 +1198,27 @@ class IS_EMAIL(Validator):
self.error_message = error_message
def __call__(self, value):
if not(isinstance(value, (basestring, unicodeT))) or not value or '@' not in value:
return (value, translate(self.error_message))
body, domain = value.rsplit('@', 1)
try:
match = self.regex.match(value)
except TypeError:
match_body = self.body_regex.match(body)
match_domain = self.domain_regex.match(domain)
if not match_domain:
# check for Internationalized Domain Names
# see https://docs.python.org/2/library/codecs.html#module-encodings.idna
domain_encoded = to_unicode(domain).encode('idna').decode('ascii')
match_domain = self.domain_regex.match(domain_encoded)
match = (match_body != None) and (match_domain != None)
except (TypeError, UnicodeError):
# Value may not be a string where we can look for matches.
# Example: we're calling ANY_OF formatter and IS_EMAIL is asked to validate a date.
match = None
if match:
domain = value.split('@')[1]
if (not self.banned or not self.banned.match(domain)) \
and (not self.forced or self.forced.match(domain)):
return (value, None)
@@ -1372,19 +1387,6 @@ unofficial_url_schemes = [
all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes
http_schemes = [None, 'http', 'https']
# This regex comes from RFC 2396, Appendix B. It's used to split a URL into
# its component parts
# Here are the regex groups that it extracts:
# scheme = group(2)
# authority = group(4)
# path = group(5)
# query = group(7)
# fragment = group(9)
url_split_regex = \
re.compile('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?')
# Defined in RFC 3490, Section 3.1, Requirement #1
# Use this regex to split the authority component of a unicode URL into
# its component labels
@@ -1454,18 +1456,15 @@ def unicode_to_ascii_authority(authority):
# We use the ToASCII operation because we are about to put the authority
# into an IDN-unaware slot
asciiLabels = []
try:
import encodings.idna
for label in labels:
if label:
asciiLabels.append(to_native(encodings.idna.ToASCII(label)))
else:
# encodings.idna.ToASCII does not accept an empty string, but
# it is necessary for us to allow for empty labels so that we
# don't modify the URL
asciiLabels.append('')
except:
asciiLabels = [str(label) for label in labels]
import encodings.idna
for label in labels:
if label:
asciiLabels.append(to_native(encodings.idna.ToASCII(label)))
else:
# encodings.idna.ToASCII does not accept an empty string, but
# it is necessary for us to allow for empty labels so that we
# don't modify the URL
asciiLabels.append('')
# RFC 3490, Section 4, Step 5
return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels))
@@ -1504,33 +1503,35 @@ def unicode_to_ascii_url(url, prepend_scheme):
"""
# convert the authority component of the URL into an ASCII punycode string,
# but encode the rest using the regular URI character encoding
groups = url_split_regex.match(url).groups()
components = urlparse.urlparse(url)
prepended = False
# If no authority was found
if not groups[3]:
if not components.netloc:
# Try appending a scheme to see if that fixes the problem
scheme_to_prepend = prepend_scheme or 'http'
groups = url_split_regex.match(
to_unicode(scheme_to_prepend) + u'://' + url).groups()
components = urlparse.urlparse(to_unicode(scheme_to_prepend) + u'://' + url)
prepended = True
# if we still can't find the authority
if not groups[3]:
if not components.netloc:
raise Exception('No authority component found, ' +
'could not decode unicode to US-ASCII')
# We're here if we found an authority, let's rebuild the URL
scheme = groups[1]
authority = groups[3]
path = groups[4] or ''
query = groups[5] or ''
fragment = groups[7] or ''
scheme = components.scheme
authority = components.netloc
path = components.path
query = components.query
fragment = components.fragment
if prepend_scheme:
scheme = str(scheme) + '://'
else:
if prepended:
scheme = ''
return scheme + unicode_to_ascii_authority(authority) +\
escape_unicode(path) + escape_unicode(query) + str(fragment)
unparsed = urlparse.urlunparse((scheme, unicode_to_ascii_authority(authority), escape_unicode(path), '', escape_unicode(query), str(fragment)))
if unparsed.startswith('//'):
unparsed = unparsed[2:] # Remove the // urlunparse puts in the beginning
return unparsed
class IS_GENERIC_URL(Validator):
@@ -1591,7 +1592,8 @@ class IS_GENERIC_URL(Validator):
% (self.prepend_scheme, self.allowed_schemes))
GENERIC_URL = re.compile(r"%[^0-9A-Fa-f]{2}|%[^0-9A-Fa-f][0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]|%$|%[0-9A-Fa-f]$|%[^0-9A-Fa-f]$")
GENERIC_URL_VALID = re.compile(r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%#]+$")
GENERIC_URL_VALID = re.compile(r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$")
URL_FRAGMENT_VALID = re.compile(r"[|A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$")
def __call__(self, value):
"""
@@ -1603,41 +1605,49 @@ class IS_GENERIC_URL(Validator):
prepended with prepend_scheme), and tuple[1] is either
None (success!) or the string error_message
"""
try:
# if the URL does not misuse the '%' character
if not self.GENERIC_URL.search(value):
# if the URL is only composed of valid characters
if self.GENERIC_URL_VALID.match(value):
# Then split up the URL into its components and check on
# the scheme
scheme = url_split_regex.match(value).group(2)
# Clean up the scheme before we check it
if not scheme is None:
scheme = urllib_unquote(scheme).lower()
# If the scheme really exists
if scheme in self.allowed_schemes:
# Then the URL is valid
return (value, None)
else:
# else, for the possible case of abbreviated URLs with
# ports, check to see if adding a valid scheme fixes
# the problem (but only do this if it doesn't have
# one already!)
if value.find('://') < 0 and None in self.allowed_schemes:
schemeToUse = self.prepend_scheme or 'http'
prependTest = self.__call__(
schemeToUse + '://' + value)
# if the prepend test succeeded
if prependTest[1] is None:
# if prepending in the output is enabled
if self.prepend_scheme:
return prependTest
else:
# else return the original,
# non-prepended value
return (value, None)
except:
pass
# if we dont have anything or the URL misuses the '%' character
if not value or self.GENERIC_URL.search(value):
return (value, translate(self.error_message))
if '#' in value:
url, fragment_part = value.split('#')
else:
url, fragment_part = value, ''
# if the URL is only composed of valid characters
if self.GENERIC_URL_VALID.match(url) and (not fragment_part or self.URL_FRAGMENT_VALID.match(fragment_part)):
# Then parse the URL into its components and check on
try:
components = urlparse.urlparse(urllib_unquote(value))._asdict()
except ValueError:
return (value, translate(self.error_message))
# Clean up the scheme before we check it
scheme = components['scheme']
if len(scheme) == 0:
scheme = None
else:
scheme = components['scheme'].lower()
# If the scheme doesn't really exists
if scheme not in self.allowed_schemes or not scheme and ':' in components['path']:
# for the possible case of abbreviated URLs with
# ports, check to see if adding a valid scheme fixes
# the problem (but only do this if it doesn't have
# one already!)
if '://' not in value and None in self.allowed_schemes:
schemeToUse = self.prepend_scheme or 'http'
prependTest = self.__call__(
schemeToUse + '://' + value)
# if the prepend test succeeded
if prependTest[1] is None:
# if prepending in the output is enabled
if self.prepend_scheme:
return prependTest
else:
return (value, None)
else:
return (value, None)
# else the URL is not valid
return (value, translate(self.error_message))
@@ -1904,15 +1914,14 @@ class IS_HTTP_URL(Validator):
(possible prepended with prepend_scheme), and tuple[1] is either
None (success!) or the string error_message
"""
try:
# if the URL passes generic validation
x = IS_GENERIC_URL(error_message=self.error_message,
allowed_schemes=self.allowed_schemes,
prepend_scheme=self.prepend_scheme)
if x(value)[1] is None:
componentsMatch = url_split_regex.match(value)
authority = componentsMatch.group(4)
components = urlparse.urlparse(value)
authority = components.netloc
# if there is an authority component
if authority:
# if authority is a valid IP address
@@ -1932,7 +1941,7 @@ class IS_HTTP_URL(Validator):
else:
# else this is a relative/abbreviated URL, which will parse
# into the URL's path component
path = componentsMatch.group(5)
path = components.path
# relative case: if this is a valid path (if it starts with
# a slash)
if path.startswith('/'):
@@ -1941,7 +1950,7 @@ class IS_HTTP_URL(Validator):
else:
# abbreviated case: if we haven't already, prepend a
# scheme and see if it fixes the problem
if value.find('://') < 0:
if '://' not in value and None in self.allowed_schemes:
schemeToUse = self.prepend_scheme or 'http'
prependTest = self.__call__(schemeToUse
+ '://' + value)
@@ -2108,7 +2117,6 @@ class IS_URL(Validator):
# If we are not able to convert the unicode url into a
# US-ASCII URL, then the URL is not valid
return (value, translate(self.error_message))
methodResult = subMethod(asciiValue)
# if the validation of the US-ASCII version of the value failed
if not methodResult[1] is None:
@@ -2470,7 +2478,7 @@ class IS_LIST_OF(Validator):
class IS_LOWER(Validator):
"""
Converts to lower case::
Converts to lowercase::
>>> IS_LOWER()('ABC')
('abc', None)
@@ -2480,12 +2488,18 @@ class IS_LOWER(Validator):
"""
def __call__(self, value):
return (to_bytes(to_unicode(value).lower()), None)
cast_back = lambda x: x
if isinstance(value, str):
cast_back = to_native
elif isinstance(value, bytes):
cast_back = to_bytes
value = to_unicode(value).lower()
return (cast_back(value), None)
class IS_UPPER(Validator):
"""
Converts to upper case::
Converts to uppercase::
>>> IS_UPPER()('abc')
('ABC', None)
@@ -2495,7 +2509,13 @@ class IS_UPPER(Validator):
"""
def __call__(self, value):
return (to_bytes(to_unicode(value).upper()), None)
cast_back = lambda x: x
if isinstance(value, str):
cast_back = to_native
elif isinstance(value, bytes):
cast_back = to_bytes
value = to_unicode(value).upper()
return (cast_back(value), None)
def urlify(s, maxlen=80, keep_underscores=False):
@@ -3025,22 +3045,22 @@ class IS_STRONG(object):
all_upper = re.findall("[A-Z]", value)
if self.upper > 0:
if not len(all_upper) >= self.upper:
failures.append(translate("Must include at least %s upper case")
failures.append(translate("Must include at least %s uppercase")
% str(self.upper))
else:
if len(all_upper) > 0:
failures.append(
translate("May not include any upper case letters"))
translate("May not include any uppercase letters"))
if isinstance(self.lower, int):
all_lower = re.findall("[a-z]", value)
if self.lower > 0:
if not len(all_lower) >= self.lower:
failures.append(translate("Must include at least %s lower case")
failures.append(translate("Must include at least %s lowercase")
% str(self.lower))
else:
if len(all_lower) > 0:
failures.append(
translate("May not include any lower case letters"))
translate("May not include any lowercase letters"))
if isinstance(self.number, int):
all_number = re.findall("[0-9]", value)
if self.number > 0:
@@ -3505,10 +3525,7 @@ class IS_IPV6(Validator):
self.error_message = error_message
def __call__(self, value):
try:
import ipaddress
except ImportError:
from gluon.contrib import ipaddr as ipaddress
from gluon._compat import ipaddress
try:
ip = ipaddress.IPv6Address(to_unicode(value))
@@ -3732,12 +3749,10 @@ class IS_IPADDRESS(Validator):
self.error_message = error_message
def __call__(self, value):
try:
from ipaddress import ip_address as IPAddress
from ipaddress import IPv6Address, IPv4Address
except ImportError:
from gluon.contrib.ipaddr import (IPAddress, IPv4Address,
IPv6Address)
from gluon._compat import ipaddress
IPAddress = ipaddress.ip_address
IPv6Address = ipaddress.IPv6Address
IPv4Address = ipaddress.IPv4Address
try:
ip = IPAddress(to_unicode(value))
+51 -40
View File
@@ -13,7 +13,7 @@ from __future__ import print_function
import datetime
import sys
from gluon._compat import StringIO, thread, xrange
from gluon._compat import StringIO, thread, xrange, PY2
import time
import threading
import os
@@ -134,24 +134,29 @@ class web2pyDialog(object):
def __init__(self, root, options):
""" web2pyDialog constructor """
import Tkinter
import tkMessageBox
if PY2:
import Tkinter as tkinter
import tkMessageBox as messagebox
else:
import tkinter
from tkinter import messagebox
bg_color = 'white'
root.withdraw()
self.root = Tkinter.Toplevel(root, bg=bg_color)
self.root = tkinter.Toplevel(root, bg=bg_color)
self.root.resizable(0, 0)
self.root.title(ProgramName)
self.options = options
self.scheduler_processes = {}
self.menu = Tkinter.Menu(self.root)
servermenu = Tkinter.Menu(self.menu, tearoff=0)
self.menu = tkinter.Menu(self.root)
servermenu = tkinter.Menu(self.menu, tearoff=0)
httplog = os.path.join(self.options.folder, self.options.log_filename)
iconphoto = os.path.join('extras', 'icons', 'web2py.gif')
if os.path.exists(iconphoto):
img = Tkinter.PhotoImage(file=iconphoto)
img = tkinter.PhotoImage(file=iconphoto)
self.root.tk.call('wm', 'iconphoto', self.root._w, img)
# Building the Menu
item = lambda: start_browser(httplog)
@@ -163,16 +168,16 @@ class web2pyDialog(object):
self.menu.add_cascade(label='Server', menu=servermenu)
self.pagesmenu = Tkinter.Menu(self.menu, tearoff=0)
self.pagesmenu = tkinter.Menu(self.menu, tearoff=0)
self.menu.add_cascade(label='Pages', menu=self.pagesmenu)
#scheduler menu
self.schedmenu = Tkinter.Menu(self.menu, tearoff=0)
self.schedmenu = tkinter.Menu(self.menu, tearoff=0)
self.menu.add_cascade(label='Scheduler', menu=self.schedmenu)
#start and register schedulers from options
self.update_schedulers(start=True)
helpmenu = Tkinter.Menu(self.menu, tearoff=0)
helpmenu = tkinter.Menu(self.menu, tearoff=0)
# Home Page
item = lambda: start_browser('http://www.web2py.com/')
@@ -180,7 +185,7 @@ class web2pyDialog(object):
command=item)
# About
item = lambda: tkMessageBox.showinfo('About web2py', ProgramInfo)
item = lambda: messagebox.showinfo('About web2py', ProgramInfo)
helpmenu.add_command(label='About',
command=item)
@@ -194,10 +199,10 @@ class web2pyDialog(object):
else:
self.root.protocol('WM_DELETE_WINDOW', self.quit)
sticky = Tkinter.NW
sticky = tkinter.NW
# Prepare the logo area
self.logoarea = Tkinter.Canvas(self.root,
self.logoarea = tkinter.Canvas(self.root,
background=bg_color,
width=300,
height=300)
@@ -206,22 +211,22 @@ class web2pyDialog(object):
logo = os.path.join('extras', 'icons', 'splashlogo.gif')
if os.path.exists(logo):
img = Tkinter.PhotoImage(file=logo)
pnl = Tkinter.Label(self.logoarea, image=img, background=bg_color, bd=0)
img = tkinter.PhotoImage(file=logo)
pnl = tkinter.Label(self.logoarea, image=img, background=bg_color, bd=0)
pnl.pack(side='top', fill='both', expand='yes')
# Prevent garbage collection of img
pnl.image = img
# Prepare the banner area
self.bannerarea = Tkinter.Canvas(self.root,
self.bannerarea = tkinter.Canvas(self.root,
bg=bg_color,
width=300,
height=300)
self.bannerarea.grid(row=1, column=1, columnspan=2, sticky=sticky)
Tkinter.Label(self.bannerarea, anchor=Tkinter.N,
tkinter.Label(self.bannerarea, anchor=tkinter.N,
text=str(ProgramVersion + "\n" + ProgramAuthor),
font=('Helvetica', 11), justify=Tkinter.CENTER,
font=('Helvetica', 11), justify=tkinter.CENTER,
foreground='#195866', background=bg_color,
height=3).pack(side='top',
fill='both',
@@ -230,24 +235,24 @@ class web2pyDialog(object):
self.bannerarea.after(1000, self.update_canvas)
# IP
Tkinter.Label(self.root,
tkinter.Label(self.root,
text='Server IP:', bg=bg_color,
justify=Tkinter.RIGHT).grid(row=4,
justify=tkinter.RIGHT).grid(row=4,
column=1,
sticky=sticky)
self.ips = {}
self.selected_ip = Tkinter.StringVar()
self.selected_ip = tkinter.StringVar()
row = 4
ips = [('127.0.0.1', 'Local (IPv4)')] + \
([('::1', 'Local (IPv6)')] if socket.has_ipv6 else []) + \
[(ip, 'Public') for ip in options.ips] + \
[('0.0.0.0', 'Public')]
for ip, legend in ips:
self.ips[ip] = Tkinter.Radiobutton(
self.ips[ip] = tkinter.Radiobutton(
self.root, bg=bg_color, highlightthickness=0,
selectcolor='light grey', width=30,
anchor=Tkinter.W, text='%s (%s)' % (legend, ip),
justify=Tkinter.LEFT,
anchor=tkinter.W, text='%s (%s)' % (legend, ip),
justify=tkinter.LEFT,
variable=self.selected_ip, value=ip)
self.ips[ip].grid(row=row, column=2, sticky=sticky)
if row == 4:
@@ -256,30 +261,30 @@ class web2pyDialog(object):
shift = row
# Port
Tkinter.Label(self.root,
tkinter.Label(self.root,
text='Server Port:', bg=bg_color,
justify=Tkinter.RIGHT).grid(row=shift,
justify=tkinter.RIGHT).grid(row=shift,
column=1, pady=10,
sticky=sticky)
self.port_number = Tkinter.Entry(self.root)
self.port_number.insert(Tkinter.END, self.options.port)
self.port_number = tkinter.Entry(self.root)
self.port_number.insert(tkinter.END, self.options.port)
self.port_number.grid(row=shift, column=2, sticky=sticky, pady=10)
# Password
Tkinter.Label(self.root,
tkinter.Label(self.root,
text='Choose Password:', bg=bg_color,
justify=Tkinter.RIGHT).grid(row=shift + 1,
justify=tkinter.RIGHT).grid(row=shift + 1,
column=1,
sticky=sticky)
self.password = Tkinter.Entry(self.root, show='*')
self.password = tkinter.Entry(self.root, show='*')
self.password.bind('<Return>', lambda e: self.start())
self.password.focus_force()
self.password.grid(row=shift + 1, column=2, sticky=sticky)
# Prepare the canvas
self.canvas = Tkinter.Canvas(self.root,
self.canvas = tkinter.Canvas(self.root,
width=400,
height=100,
bg='black')
@@ -288,19 +293,19 @@ class web2pyDialog(object):
self.canvas.after(1000, self.update_canvas)
# Prepare the frame
frame = Tkinter.Frame(self.root)
frame = tkinter.Frame(self.root)
frame.grid(row=shift + 3, column=1, columnspan=2, pady=5,
sticky=sticky)
# Start button
self.button_start = Tkinter.Button(frame,
self.button_start = tkinter.Button(frame,
text='start server',
command=self.start)
self.button_start.grid(row=0, column=0, sticky=sticky)
# Stop button
self.button_stop = Tkinter.Button(frame,
self.button_stop = tkinter.Button(frame,
text='stop server',
command=self.stop)
@@ -454,9 +459,12 @@ class web2pyDialog(object):
def error(self, message):
""" Shows error message """
import tkMessageBox
tkMessageBox.showerror('web2py start server', message)
if PY2:
import tkMessageBox as messagebox
else:
from tkinter import messagebox
messagebox.showerror('web2py start server', message)
def start(self):
""" Starts web2py server """
@@ -1191,10 +1199,13 @@ def start(cron=True):
if not options.nogui and options.password == '<ask>':
try:
import Tkinter
if PY2:
import Tkinter as tkinter
else:
import tkinter
havetk = True
try:
root = Tkinter.Tk()
root = tkinter.Tk()
except:
pass
except (ImportError, OSError):
+2 -1
View File
@@ -15,6 +15,7 @@ except ImportError:
import readline
try:
from gluon import DAL
from gluon.fileutils import open_file
except ImportError as err:
print('gluon path not found')
@@ -531,7 +532,7 @@ class setCopyDB():
self.db.export_to_csv_file(open('tmp.sql', 'wb'))
print 'importing data...'
other_db.import_from_csv_file(open('tmp.sql', 'rb'))
other_db.import_from_csv_file(open_file('tmp.sql', 'rb'))
other_db.commit()
print 'done!'
print 'Attention: do not run this program again or you end up with duplicate records'
+13 -7
View File
@@ -18,7 +18,7 @@ legacy_db(legacy_db.mytable.id>0).select()
If the script crashes this is might be due to that fact that the data_type_map dictionary below is incomplete.
Please complete it, improve it and continue.
Created by Falko Krause, minor modifications by Massimo Di Pierro and Ron McOuat
Created by Falko Krause, minor modifications by Massimo Di Pierro, Ron McOuat and Marvi Benedet
'''
import subprocess
import re
@@ -52,10 +52,11 @@ data_type_map = dict(
)
def mysql(database_name, username, password):
def mysql(database_name, username, password, host):
p = subprocess.Popen(['mysql',
'--user=%s' % username,
'--password=%s' % password,
'--host=%s' % host,
'--execute=show tables;',
database_name],
stdin=subprocess.PIPE,
@@ -64,14 +65,15 @@ def mysql(database_name, username, password):
sql_showtables, stderr = p.communicate()
tables = [re.sub(
'\|\s+([^\|*])\s+.*', '\1', x) for x in sql_showtables.split()[1:]]
connection_string = "legacy_db = DAL('mysql://%s:%s@localhost/%s')" % (
username, password, database_name)
connection_string = "legacy_db = DAL('mysql://%s:%s@%s/%s')" % (
username, password, host, database_name)
legacy_db_table_web2py_code = []
for table_name in tables:
#get the sql create statement
p = subprocess.Popen(['mysqldump',
'--user=%s' % username,
'--password=%s' % password,
'--host=%s' % host,
'--skip-add-drop-table',
'--no-data', database_name,
table_name], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -106,9 +108,13 @@ def mysql(database_name, username, password):
legacy_db_table_web2py_code)
return legacy_db_web2py_code
regex = re.compile('(.*?):(.*?)@(.*)')
regex = re.compile('(.*):(.*)@((.*)/)?(.*)')
if len(sys.argv) < 2 or not regex.match(sys.argv[1]):
print 'USAGE:\n\n extract_mysql_models.py username:password@data_basename\n\n'
print 'USAGE:\n\n extract_mysql_models.py username:password@[host/]data_basename\n\n'
else:
m = regex.match(sys.argv[1])
print mysql(m.group(3), m.group(1), m.group(2))
username = m.group(1)
password = m.group(2)
host = m.group(4) or 'localhost'
db_name = m.group(5)
print mysql(database_name = db_name, username = username, password = password, host = host)
@@ -216,6 +216,7 @@ fi
/etc/init.d/nginx start
systemctl start emperor.uwsgi.service
systemctl enable emperor.uwsgi.service
echo <<EOF
you can stop uwsgi and nginx with